One discoverable home for every paid extra, so the pricing model is legible instead of scattered across the Sasquatch panel and each offer form: - components/ProSupportScreen.tsx (Settings ?section=pro + a menu row): honest "prepaid, not a subscription" framing; Sasquatch Pro status (days left) + PRO_TIERS with per-day sats and a Go Pro / Extend button; Featured placement (PLATFORM_FEES x DURATION_TIERS with per-day + best-value); your currently featured Lab listings (featuredUntil); and the free paths (PoW listing, free AI FAQ). - Distinct naming in copy: Sasquatch Pro (AI) vs Featured placement (visibility). - Smoke test now walks /settings?section=pro.
158 lines
8.4 KiB
TypeScript
158 lines
8.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { ChevronLeft, Crown, CheckCircle2, Sparkles, Cpu } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useTranslation } from '@/contexts/language';
|
|
import { useSasquatchPro } from '@/hooks/useSasquatchPro';
|
|
import { PRO_TIERS } from '@/lib/sasquatchPro';
|
|
import { DURATION_TIERS, PLATFORM_FEES, FEE_META, tierPriceSats, tierPerDaySats, type FeeType } from '@/hooks/usePlatformFee';
|
|
import { SasquatchProDialog } from '@/components/SasquatchProDialog';
|
|
import { useLabProfiles } from '@/hooks/useLabProfiles';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { labProfilePath } from '@/lib/labRoutes';
|
|
import { dateLocaleCode } from '@/lib/dateFnsI18n';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* Settings → Pro & Support.
|
|
*
|
|
* One honest home for every paid extra: what it does, the duration tiers and
|
|
* effective per-day price, your current status, and the free alternatives.
|
|
* Nothing here holds funds — each purchase is a one-off Lightning payment.
|
|
*/
|
|
export function ProSupportScreen({ onBack }: { onBack: () => void }) {
|
|
const { t } = useTranslation();
|
|
const { isPro, expiresAt } = useSasquatchPro();
|
|
const { user } = useCurrentUser();
|
|
const { data: profiles } = useLabProfiles(100);
|
|
const [proOpen, setProOpen] = useState(false);
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const daysLeft = isPro && expiresAt ? Math.max(0, Math.ceil((expiresAt - now) / 86400)) : 0;
|
|
const fmt = (secs: number) => new Date(secs * 1000).toLocaleDateString(dateLocaleCode(), { month: 'short', day: 'numeric', year: 'numeric' });
|
|
|
|
const featured = (profiles ?? []).filter((p) => p.pubkey === user?.pubkey && p.featuredUntil && p.featuredUntil > now);
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-5">
|
|
<button
|
|
onClick={onBack}
|
|
aria-label={t('common.back')}
|
|
className="flex items-center justify-center w-8 h-8 rounded-xl bg-white/5 hover:bg-white/10 active:scale-95 transition-all"
|
|
>
|
|
<ChevronLeft className="h-4 w-4 text-white" />
|
|
</button>
|
|
<h2 className="text-lg font-black uppercase tracking-wide text-white">{t('settingsUi.proSupport.title')}</h2>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Honest framing */}
|
|
<div className="rounded-2xl border border-[#ccff00]/20 bg-[#ccff00]/5 p-4">
|
|
<p className="flex items-center gap-2 text-sm font-bold text-[#ccff00]">
|
|
<CheckCircle2 className="h-4 w-4" /> {t('settingsUi.proSupport.prepaidTitle')}
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground leading-relaxed">{t('settingsUi.proSupport.prepaidBody')}</p>
|
|
</div>
|
|
|
|
{/* Sasquatch Pro */}
|
|
<div className="rounded-2xl border border-white/8 p-4 bg-white/2">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-emerald-500/15">
|
|
<Sparkles className="h-4 w-4 text-emerald-400" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-black uppercase tracking-wide text-white">{t('settingsUi.proSupport.sasquatchTitle')}</p>
|
|
<p className="mt-0.5 text-xs text-muted-foreground leading-relaxed">{t('settingsUi.proSupport.sasquatchBenefit')}</p>
|
|
<p className={cn('mt-1 text-xs font-bold', isPro ? 'text-emerald-400' : 'text-gray-500')}>
|
|
{isPro
|
|
? expiresAt
|
|
? `${t('settingsUi.proSupport.activeUntil', { date: fmt(expiresAt) })} · ${t('settingsUi.proSupport.daysLeft', { count: daysLeft })}`
|
|
: t('settingsUi.proSupport.active')
|
|
: t('settingsUi.proSupport.notActive')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
{PRO_TIERS.map((tier) => (
|
|
<div key={tier.id} className={cn('rounded-xl border p-2.5 text-center', tier.popular ? 'border-emerald-500/50 bg-emerald-500/10' : 'border-white/10')}>
|
|
<p className="text-xs font-bold text-white">{t(tier.labelKey)}</p>
|
|
<p className="mt-1 text-sm font-black text-white">{tier.sats.toLocaleString()}</p>
|
|
<p className="text-[10px] text-muted-foreground">{t('common.sats')} · {Math.round(tier.sats / tier.days)}/{t('settingsUi.proSupport.day')}</p>
|
|
{tier.badgeKey && <p className="mt-1 text-[10px] font-bold text-emerald-400">{t(tier.badgeKey)}</p>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Button onClick={() => setProOpen(true)} className="mt-3 w-full gap-2 bg-[#ccff00] font-black uppercase text-black hover:bg-[#ccff00]/90">
|
|
<Crown className="h-4 w-4" /> {isPro ? t('settingsUi.proSupport.extend') : t('settingsUi.proSupport.goPro')}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Featured placement */}
|
|
<div className="rounded-2xl border border-white/8 p-4 bg-white/2">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-amber-500/15">
|
|
<Cpu className="h-4 w-4 text-amber-400" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-black uppercase tracking-wide text-white">{t('settingsUi.proSupport.featuredTitle')}</p>
|
|
<p className="mt-0.5 text-xs text-muted-foreground leading-relaxed">{t('settingsUi.proSupport.featuredBenefit')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 space-y-3">
|
|
{(Object.keys(PLATFORM_FEES) as FeeType[]).map((type) => (
|
|
<div key={type}>
|
|
<p className="text-xs font-bold text-gray-300">
|
|
<span className="mr-1">{FEE_META[type].emoji}</span>{FEE_META[type].label}
|
|
<span className="ml-2 font-normal text-muted-foreground">{FEE_META[type].benefit}</span>
|
|
</p>
|
|
<div className="mt-1.5 grid grid-cols-2 gap-1.5 sm:grid-cols-4">
|
|
{DURATION_TIERS.map((tier) => (
|
|
<div key={tier.id} className={cn('rounded-lg border p-2 text-center', tier.bestValue ? 'border-[#ccff00]/40 bg-[#ccff00]/5' : 'border-white/10')}>
|
|
<p className="text-[11px] font-bold text-white">{tier.label}</p>
|
|
<p className="text-xs font-black text-white">{tierPriceSats(type, tier).toLocaleString()}</p>
|
|
<p className="text-[10px] text-muted-foreground">{tierPerDaySats(type, tier).toLocaleString()}/{t('settingsUi.proSupport.day')}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="mt-3 text-xs text-muted-foreground">{t('settingsUi.proSupport.featuredHow')}</p>
|
|
</div>
|
|
|
|
{/* Your featured listings */}
|
|
<div className="rounded-2xl border border-white/8 p-4 bg-white/2">
|
|
<p className="text-sm font-black uppercase tracking-wide text-white">{t('settingsUi.proSupport.yourFeatured')}</p>
|
|
{featured.length === 0 ? (
|
|
<p className="mt-1 text-xs text-muted-foreground">{t('settingsUi.proSupport.noneFeatured')}</p>
|
|
) : (
|
|
<div className="mt-2 space-y-1.5">
|
|
{featured.map((p) => (
|
|
<Link
|
|
key={p.id}
|
|
to={labProfilePath(p.profileType, p.pubkey, p.identifier)}
|
|
className="flex items-center justify-between gap-2 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs font-bold text-gray-200 hover:bg-white/10"
|
|
>
|
|
<span className="truncate">{p.title}</span>
|
|
<span className="flex-shrink-0 text-amber-400">{t('settingsUi.proSupport.featuredUntil', { date: fmt(p.featuredUntil!) })}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Free paths */}
|
|
<div className="rounded-2xl border border-white/8 p-4 bg-white/2">
|
|
<p className="text-sm font-black uppercase tracking-wide text-white">{t('settingsUi.proSupport.freeTitle')}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground leading-relaxed">{t('settingsUi.proSupport.freeBody')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<SasquatchProDialog open={proOpen} onOpenChange={setProOpen} />
|
|
</div>
|
|
);
|
|
}
|