Rework Lab fee into optional "Pro" upgrade (Option B, no paywall)

Basic Lab profiles are now always free to publish. The 5,000 sat fee is
opt-in and unlocks a Pro badge + featured placement instead of gating
publication behind a rate.

- types.ts: `featuredUntil` field on all 5 Lab types + SkateEvent,
  parseFeaturedUntil() + isFeaturedActive() helpers, wired into parsers
- usePlatformFee: exposes featuredUntilSeconds for the `featured` tag
  (platform owners get a 1yr window)
- New ProBadge + LabProUpgrade (opt-in toggle → reveals PlatformFeePayment)
- All 5 Become pages: removed rate-based fee gate and wallet-required gate;
  publish is free; when Pro is active a `["featured", "<expiry>"]` tag is added

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-24 15:31:49 +07:00
parent 77dc4b562c
commit 5c0d4715d5
9 changed files with 250 additions and 98 deletions

View file

@ -0,0 +1,100 @@
import { Sparkles, Check } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { usePlatformFee } from '@/hooks/usePlatformFee';
import { cn } from '@/lib/utils';
interface LabProUpgradeProps {
/** Whether the user opted in to Pro. */
enabled: boolean;
onEnabledChange: (enabled: boolean) => void;
/** Accent color class for the entity (e.g. "text-orange-400"). */
accentText?: string;
className?: string;
}
/**
* Opt-in "Go Pro" upgrade for Lab profiles.
*
* Free profiles publish without this. Toggling it on reveals the Lightning fee
* payment; once paid (or for platform owners), the profile is published with a
* `featured` tag that unlocks the Pro badge + featured placement in The Lab.
*
* There is NO hard paywall — a basic profile is always free.
*/
export function LabProUpgrade({
enabled,
onEnabledChange,
accentText = 'text-amber-400',
className,
}: LabProUpgradeProps) {
const { isActive, isPlatformOwner, amountSats, daysRemaining } = usePlatformFee('labProfile');
return (
<div
className={cn(
'rounded-2xl border border-amber-500/30 bg-gradient-to-br from-amber-500/8 to-black/40 overflow-hidden',
className,
)}
>
<div className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 min-w-0">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center flex-shrink-0 shadow-lg shadow-amber-500/25">
<Sparkles className="h-4 w-4 text-black" />
</div>
<div className="min-w-0">
<p className={cn('font-black uppercase tracking-wide text-sm', accentText)}>
Go Pro <span className="text-gray-500 font-normal normal-case">· optional</span>
</p>
<p className="text-xs text-gray-400 mt-0.5">
Free to list. Pro adds a <strong className="text-amber-400">Pro badge</strong> and
{' '}<strong className="text-amber-400">featured placement</strong> at the top of The Lab.
</p>
</div>
</div>
{isActive ? (
<span className="flex items-center gap-1 text-[11px] font-bold text-green-400 flex-shrink-0">
<Check className="h-3.5 w-3.5" /> Active
</span>
) : (
<Switch
checked={enabled}
onCheckedChange={onEnabledChange}
className="flex-shrink-0 data-[state=checked]:bg-amber-500"
/>
)}
</div>
{/* Benefits list */}
{!isActive && (
<ul className="mt-3 grid gap-1.5 text-xs text-gray-400">
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Gold "Pro" badge on your profile &amp; cards</li>
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Featured first when skaters browse The Lab</li>
<li className="flex items-center gap-2"><Check className="h-3.5 w-3.5 text-amber-400 flex-shrink-0" /> Supports SkateSpots — no ads, no middlemen</li>
</ul>
)}
{/* Already active */}
{isActive && (
<p className="mt-2 text-xs text-gray-500">
{isPlatformOwner
? 'Platform owner — Pro is always on.'
: `Pro active · ${daysRemaining} day${daysRemaining !== 1 ? 's' : ''} remaining. Your profile will be featured.`}
</p>
)}
</div>
{/* Payment appears once opted in and not already active */}
{enabled && !isActive && (
<div className="px-4 pb-4">
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates isActive */ }}
context={`One-time ${amountSats.toLocaleString()} sat fee for 30 days of Pro placement. Your profile stays live for free after it expires — you just lose the badge & boost.`}
/>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,34 @@
import { Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { isFeaturedActive } from '@/lib/types';
interface ProBadgeProps {
/** The `featuredUntil` unix-seconds timestamp from a parsed entity. */
featuredUntil?: number;
className?: string;
/** Compact = icon only (for dense card corners). */
compact?: boolean;
}
/**
* "Pro" badge shown on featured Lab profiles and events.
* Renders nothing if the entity isn't currently featured.
*/
export function ProBadge({ featuredUntil, className, compact = false }: ProBadgeProps) {
if (!isFeaturedActive(featuredUntil)) return null;
return (
<Badge
className={cn(
'gap-1 border-0 bg-gradient-to-r from-amber-400 to-orange-500 text-black font-black uppercase tracking-wide shadow-sm shadow-orange-500/30',
compact ? 'px-1.5 py-0 text-[9px]' : 'text-[10px]',
className,
)}
title="Pro listing — this profile supports SkateSpots and is featured."
>
<Sparkles className={compact ? 'h-2.5 w-2.5' : 'h-3 w-3'} />
Pro
</Badge>
);
}

View file

@ -77,6 +77,16 @@ export function usePlatformFee(type: FeeType) {
? Math.ceil((record.expiresAt - Date.now()) / (24 * 60 * 60 * 1000))
: 0;
// Expiry as unix SECONDS, for writing into a `featured` event tag.
// Platform owners get a long-lived (1 year) featured window.
const featuredUntilSeconds = (() => {
if (isPlatformOwner) {
return Math.floor((Date.now() + 365 * 24 * 60 * 60 * 1000) / 1000);
}
if (record) return Math.floor(record.expiresAt / 1000);
return undefined;
})();
// Record a successful payment
const recordPayment = () => {
const now = Date.now();
@ -98,6 +108,8 @@ export function usePlatformFee(type: FeeType) {
isActive,
/** Days remaining on active fee */
daysRemaining,
/** Expiry in unix seconds — write into a `featured` tag when publishing */
featuredUntilSeconds,
/** Platform owners bypass everything */
isPlatformOwner,
/** Call this after successful payment */

View file

@ -88,6 +88,30 @@ export function parseMemberPubkeys(event: NostrEvent): string[] {
/** All Lab provider kinds (The Lab service profiles). */
export const LAB_KINDS = [35001, 35002, 35003, 35004, 35005] as const;
/**
* "Pro" / featured status for Lab profiles and events.
*
* When a user pays the optional Pro/featured listing fee, the published event
* carries a `["featured", "<unix-seconds-expiry>"]` tag. This is *self-attested*
* (the app records payment and writes the tag) rather than cryptographically
* verified against a real Lightning payment — the same trust level the rest of
* the platform-fee flow already uses. A future enhancement could verify against
* NIP-57 zap receipts to the platform Lightning address.
*/
export function parseFeaturedUntil(event: NostrEvent): number | undefined {
const raw = event.tags.find(([name]) => name === 'featured')?.[1];
if (!raw) return undefined;
const ts = parseInt(raw, 10);
if (isNaN(ts) || ts <= 0) return undefined;
return ts; // unix seconds
}
/** Is a `featuredUntil` timestamp (unix seconds) still active right now? */
export function isFeaturedActive(featuredUntil: number | undefined): boolean {
if (!featuredUntil) return false;
return featuredUntil * 1000 > Date.now();
}
/**
* A parsed addressable coordinate: `<kind>:<pubkey>:<d>`.
*/
@ -198,6 +222,7 @@ export interface SkateEvent {
location?: string;
image?: string;
types: string[];
featuredUntil?: number; // Featured event listing expiry (unix seconds), if paid
createdAt: number;
event: NostrEvent;
}
@ -389,6 +414,7 @@ export function parseSkateEvent(event: NostrEvent): SkateEvent | null {
location,
image,
types,
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};
@ -847,6 +873,7 @@ export interface SkateBuilder {
id: string;
pubkey: string;
identifier: string;
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
title: string;
bio: string;
geohash?: string;
@ -876,6 +903,7 @@ export interface SkateCoach {
id: string;
pubkey: string;
identifier: string;
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
title: string;
bio: string;
geohash?: string;
@ -901,6 +929,7 @@ export interface SkateFilmer {
id: string;
pubkey: string;
identifier: string;
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
title: string;
bio: string;
geohash?: string;
@ -928,6 +957,7 @@ export interface SkateEditor {
id: string;
pubkey: string;
identifier: string;
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
title: string;
bio: string;
geohash?: string;
@ -955,6 +985,7 @@ export interface SkatePhotographer {
id: string;
pubkey: string;
identifier: string;
featuredUntil?: number; // "Pro" listing expiry (unix seconds), if paid
title: string;
bio: string;
geohash?: string;
@ -1089,6 +1120,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null {
members,
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
crewRef: parseCrewRef(event),
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};
@ -1147,6 +1179,7 @@ export function parseSkateFilmer(event: NostrEvent): SkateFilmer | null {
members,
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
crewRef: parseCrewRef(event),
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};
@ -1202,6 +1235,7 @@ export function parseSkateEditor(event: NostrEvent): SkateEditor | null {
entityType,
members,
crewRef: parseCrewRef(event),
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};
@ -1259,6 +1293,7 @@ export function parseSkatePhotographer(event: NostrEvent): SkatePhotographer | n
members,
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
crewRef: parseCrewRef(event),
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};
@ -1320,6 +1355,7 @@ export function parseSkateBuilder(event: NostrEvent): SkateBuilder | null {
members,
spotRefs: spotRefs.length > 0 ? spotRefs : undefined,
crewRef: parseCrewRef(event),
featuredUntil: parseFeaturedUntil(event),
createdAt: event.created_at,
event,
};

View file

@ -33,8 +33,8 @@ import {
type EntityType,
} from '@/lib/types';
import { cn } from '@/lib/utils';
import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap, Hammer } from 'lucide-react';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { HardHat, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Hammer } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
const SERVICE_ENTRIES = Object.entries(BUILDER_SERVICE_LABELS);
@ -86,7 +86,8 @@ export default function BecomeBuilderPage() {
const { webln, activeNWC } = useWallet();
const hasWallet = !!webln || !!activeNWC;
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
@ -221,6 +222,11 @@ export default function BecomeBuilderPage() {
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
}
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({
kind: SKATE_BUILDER_KIND,
content: bio.trim(),
@ -523,28 +529,18 @@ export default function BecomeBuilderPage() {
)}
</div>
{/* Platform fee gate — only when a rate is set */}
{rate.trim() && !feePaid && !isPlatformOwner && (
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates */ }}
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
/>
)}
{/* Optional Pro upgrade — free to list without it */}
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-amber-400" />
<Button
type="submit"
className="w-full bg-amber-500 hover:bg-amber-600 text-black uppercase font-black"
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
) : !hasWallet ? (
<><Wallet className="h-4 w-4 mr-2" />CONNECT WALLET TO CONTINUE</>
) : rate.trim() && !feePaid && !isPlatformOwner ? (
<><Zap className="h-4 w-4 mr-2" />PAY LISTING FEE ABOVE TO CONTINUE</>
) : (
<><HardHat className="h-4 w-4 mr-2" />CREATE BUILDER PROFILE</>
<><HardHat className="h-4 w-4 mr-2" />{isPro ? 'CREATE PRO BUILDER PROFILE' : 'CREATE BUILDER PROFILE'}</>
)}
</Button>
</CardContent>

View file

@ -23,8 +23,8 @@ import { EntityTypeField } from '@/components/EntityTypeField';
import { CrewLinkField } from '@/components/CrewLinkField';
import { SKATE_COACH_KIND, SKATE_SPOT_KIND, COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
import { cn } from '@/lib/utils';
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap } from 'lucide-react';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
const SKILL_ENTRIES = Object.entries(COACH_SKILL_LABELS);
@ -46,8 +46,9 @@ export default function BecomeCoachPage() {
// Check if user has any wallet connected
const hasWallet = !!webln || !!activeNWC;
// Platform listing fee — required when a rate is set
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
// Optional "Pro" upgrade — free to list; Pro adds a badge + featured placement
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
@ -172,6 +173,11 @@ export default function BecomeCoachPage() {
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
}
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({
kind: SKATE_COACH_KIND,
content: bio.trim(),
@ -441,39 +447,23 @@ export default function BecomeCoachPage() {
)}
</div>
{/* Platform fee gate — only when a rate is set */}
{rate.trim() && !feePaid && !isPlatformOwner && (
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates */ }}
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
/>
)}
{/* Optional Pro upgrade — free to list without it */}
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" />
<Button
type="submit"
className="w-full bg-red-500 hover:bg-red-600 uppercase font-black"
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Publishing...
</>
) : !hasWallet ? (
<>
<Wallet className="h-4 w-4 mr-2" />
CONNECT WALLET TO CONTINUE
</>
) : rate.trim() && !feePaid && !isPlatformOwner ? (
<>
<Zap className="h-4 w-4 mr-2" />
PAY LISTING FEE ABOVE TO CONTINUE
</>
) : (
<>
<GraduationCap className="h-4 w-4 mr-2" />
CREATE COACH PROFILE
{isPro ? 'CREATE PRO COACH PROFILE' : 'CREATE COACH PROFILE'}
</>
)}
</Button>

View file

@ -30,8 +30,8 @@ import {
import { EntityTypeField } from '@/components/EntityTypeField';
import { CrewLinkField } from '@/components/CrewLinkField';
import { cn } from '@/lib/utils';
import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor, Zap } from 'lucide-react';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
const SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS);
@ -54,7 +54,8 @@ export default function BecomeEditorPage() {
const hasWallet = !!webln || !!activeNWC;
// Platform listing fee — required when a rate is set
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
@ -171,6 +172,11 @@ export default function BecomeEditorPage() {
if (hardware.trim()) tags.push(['hardware', hardware.trim()]);
if (software.trim()) tags.push(['software', software.trim()]);
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({ kind: SKATE_EDITOR_KIND, content: bio.trim(), tags });
toast({ title: 'Editor profile created!', description: 'Your profile has been published to SkateSpots.' });
@ -426,16 +432,10 @@ export default function BecomeEditorPage() {
</CardContent>
</Card>
{/* Platform fee gate — only when a rate is set */}
{rate.trim() && !feePaid && !isPlatformOwner && (
{/* Optional Pro upgrade — free to list without it */}
<div className="mt-4">
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates */ }}
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
/>
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-purple-400" />
</div>
)}
<div className="mt-6 flex gap-3">
<Link to="/lab" className="flex-1">
@ -443,15 +443,13 @@ export default function BecomeEditorPage() {
</Link>
<Button
type="submit"
disabled={isPublishing || selectedServices.length === 0 || (!!rate.trim() && !feePaid && !isPlatformOwner)}
disabled={isPublishing || selectedServices.length === 0}
className="flex-1 bg-purple-600 hover:bg-purple-700 font-black uppercase gap-2"
>
{isPublishing ? (
<><Loader2 className="h-4 w-4 animate-spin" />Publishing…</>
) : rate.trim() && !feePaid && !isPlatformOwner ? (
<><Zap className="h-4 w-4" />Pay Fee Above</>
) : (
<><Scissors className="h-4 w-4" />Publish Editor Profile</>
<><Scissors className="h-4 w-4" />{isPro ? 'Publish Pro Editor Profile' : 'Publish Editor Profile'}</>
)}
</Button>
</div>

View file

@ -23,8 +23,8 @@ import { EntityTypeField } from '@/components/EntityTypeField';
import { CrewLinkField } from '@/components/CrewLinkField';
import { SKATE_FILMER_KIND, SKATE_SPOT_KIND, FILMING_SERVICE_LABELS, FILMING_SERVICE_DESCRIPTIONS, FILMING_SKILL_LABELS, FILMING_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types';
import { cn } from '@/lib/utils';
import { Video, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap } from 'lucide-react';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { Video, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
const SERVICE_ENTRIES = Object.entries(FILMING_SERVICE_LABELS);
@ -48,7 +48,8 @@ export default function BecomeFilmerPage() {
const hasWallet = !!webln || !!activeNWC;
// Platform listing fee — required when a rate is set
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
@ -188,6 +189,11 @@ export default function BecomeFilmerPage() {
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
}
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({
kind: SKATE_FILMER_KIND,
content: bio.trim(),
@ -499,39 +505,23 @@ export default function BecomeFilmerPage() {
)}
</div>
{/* Platform fee gate — only when a rate is set */}
{rate.trim() && !feePaid && !isPlatformOwner && (
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates */ }}
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
/>
)}
{/* Optional Pro upgrade — free to list without it */}
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-red-400" />
<Button
type="submit"
className="w-full bg-red-500 hover:bg-red-600 uppercase font-black"
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Publishing...
</>
) : !hasWallet ? (
<>
<Wallet className="h-4 w-4 mr-2" />
CONNECT WALLET TO CONTINUE
</>
) : rate.trim() && !feePaid && !isPlatformOwner ? (
<>
<Zap className="h-4 w-4 mr-2" />
PAY LISTING FEE ABOVE TO CONTINUE
</>
) : (
<>
<Video className="h-4 w-4 mr-2" />
CREATE FILMING PROFILE
{isPro ? 'CREATE PRO FILMING PROFILE' : 'CREATE FILMING PROFILE'}
</>
)}
</Button>

View file

@ -35,8 +35,8 @@ import {
isValidNoffer,
} from '@/lib/types';
import { cn } from '@/lib/utils';
import { Camera, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Zap, Aperture } from 'lucide-react';
import { PlatformFeePayment } from '@/components/PlatformFeePayment';
import { Camera, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Aperture } from 'lucide-react';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
const SERVICE_ENTRIES = Object.entries(PHOTO_SERVICE_LABELS);
@ -92,7 +92,8 @@ export default function BecomePhotographerPage() {
const { webln, activeNWC } = useWallet();
const hasWallet = !!webln || !!activeNWC;
const { isActive: feePaid, isPlatformOwner } = usePlatformFee('labProfile');
const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile');
const [goPro, setGoPro] = useState(false);
const [bio, setBio] = useState('');
const [location, setLocation] = useState('');
@ -238,6 +239,11 @@ export default function BecomePhotographerPage() {
tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]);
}
// Pro / featured listing (optional, paid)
if (isPro && featuredUntilSeconds) {
tags.push(['featured', String(featuredUntilSeconds)]);
}
await createEvent({
kind: SKATE_PHOTOGRAPHER_KIND,
content: bio.trim(),
@ -545,28 +551,18 @@ export default function BecomePhotographerPage() {
)}
</div>
{/* Platform fee gate — only when a rate is set */}
{rate.trim() && !feePaid && !isPlatformOwner && (
<PlatformFeePayment
type="labProfile"
onSuccess={() => { /* hook auto-updates */ }}
context={`You set a rate of "${rate}" — a 5,000 sat listing fee is required to publish monetized profiles.`}
/>
)}
{/* Optional Pro upgrade — free to list without it */}
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-indigo-400" />
<Button
type="submit"
className="w-full bg-indigo-600 hover:bg-indigo-700 uppercase font-black"
disabled={isPublishing || isUploading || !hasWallet || (!!rate.trim() && !feePaid && !isPlatformOwner)}
disabled={isPublishing || isUploading}
>
{isPublishing ? (
<><Loader2 className="h-4 w-4 mr-2 animate-spin" />Publishing...</>
) : !hasWallet ? (
<><Wallet className="h-4 w-4 mr-2" />CONNECT WALLET TO CONTINUE</>
) : rate.trim() && !feePaid && !isPlatformOwner ? (
<><Zap className="h-4 w-4 mr-2" />PAY LISTING FEE ABOVE TO CONTINUE</>
) : (
<><Camera className="h-4 w-4 mr-2" />CREATE PHOTOGRAPHY PROFILE</>
<><Camera className="h-4 w-4 mr-2" />{isPro ? 'CREATE PRO PHOTOGRAPHY PROFILE' : 'CREATE PHOTOGRAPHY PROFILE'}</>
)}
</Button>
</CardContent>