From 4cad20171671f38961537adf904e5c5c15a3b421 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 14:28:13 +0700 Subject: [PATCH] =?UTF-8?q?Add=20a=20Settings=20=E2=86=92=20Pro=20&=20Supp?= =?UTF-8?q?ort=20hub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AGENTS.md | 11 ++ scripts/smoke-test.mjs | 1 + src/components/ProSupportScreen.tsx | 158 ++++++++++++++++++++++++++++ src/i18n/en.ts | 23 ++++ src/i18n/es.ts | 23 ++++ src/pages/SettingsPage.tsx | 17 ++- 6 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 src/components/ProSupportScreen.tsx diff --git a/AGENTS.md b/AGENTS.md index 7eeef24..c41bf45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -542,6 +542,17 @@ session with **no other memory of this project**. So: Pro access (`useSasquatchPro`) is granted by a provider-authored zap receipt, and its **expiry is derived from the paid amount → tier**, never the user-signed `pro-expiry` tag. + **Pro & Support hub (2026-10-04):** all paid extras now have one home — + **Settings → Pro & Support** (`?section=pro`, `components/ProSupportScreen.tsx`, + menu row + section in `SettingsPage`). It shows the honest "prepaid, not a + subscription" framing, **Sasquatch Pro** status (days left) + `PRO_TIERS` with + per-day sats + a "Go Pro / Extend" button (opens `SasquatchProDialog`), the + **Featured placement** types (`PLATFORM_FEES` × `DURATION_TIERS`, per-day + + best-value), **your currently-featured Lab listings** (`featuredUntil`), and + the **free paths** (PoW listing, free AI FAQ). Distinct naming is enforced in + copy: **Sasquatch Pro** (AI) vs **Featured placement** (visibility) — don't + call both "Pro". Not done: an actual discounted bundle SKU (would be a single + combined zap, no new rails). `/settings?section=pro` is in the smoke list. - **Pro proofs STACK (fixed 2026-09-28).** `useSasquatchPro` now collects ALL valid proofs and accumulates `expiry = max(expiry, proofTime) + tierDays` (renewal semantics) — before, it took the **max** of diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index aefe887..de142dc 100755 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -138,6 +138,7 @@ const ROUTES = [ '/settings', '/settings?section=profile', '/settings?section=coordinator', + '/settings?section=pro', '/npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx', '/stash', '/collections', diff --git a/src/components/ProSupportScreen.tsx b/src/components/ProSupportScreen.tsx new file mode 100644 index 0000000..ca0bb7f --- /dev/null +++ b/src/components/ProSupportScreen.tsx @@ -0,0 +1,158 @@ +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 ( +
+
+ +

{t('settingsUi.proSupport.title')}

+
+ +
+ {/* Honest framing */} +
+

+ {t('settingsUi.proSupport.prepaidTitle')} +

+

{t('settingsUi.proSupport.prepaidBody')}

+
+ + {/* Sasquatch Pro */} +
+
+
+ +
+
+

{t('settingsUi.proSupport.sasquatchTitle')}

+

{t('settingsUi.proSupport.sasquatchBenefit')}

+

+ {isPro + ? expiresAt + ? `${t('settingsUi.proSupport.activeUntil', { date: fmt(expiresAt) })} · ${t('settingsUi.proSupport.daysLeft', { count: daysLeft })}` + : t('settingsUi.proSupport.active') + : t('settingsUi.proSupport.notActive')} +

+
+
+ +
+ {PRO_TIERS.map((tier) => ( +
+

{t(tier.labelKey)}

+

{tier.sats.toLocaleString()}

+

{t('common.sats')} · {Math.round(tier.sats / tier.days)}/{t('settingsUi.proSupport.day')}

+ {tier.badgeKey &&

{t(tier.badgeKey)}

} +
+ ))} +
+ + +
+ + {/* Featured placement */} +
+
+
+ +
+
+

{t('settingsUi.proSupport.featuredTitle')}

+

{t('settingsUi.proSupport.featuredBenefit')}

+
+
+ +
+ {(Object.keys(PLATFORM_FEES) as FeeType[]).map((type) => ( +
+

+ {FEE_META[type].emoji}{FEE_META[type].label} + {FEE_META[type].benefit} +

+
+ {DURATION_TIERS.map((tier) => ( +
+

{tier.label}

+

{tierPriceSats(type, tier).toLocaleString()}

+

{tierPerDaySats(type, tier).toLocaleString()}/{t('settingsUi.proSupport.day')}

+
+ ))} +
+
+ ))} +
+

{t('settingsUi.proSupport.featuredHow')}

+
+ + {/* Your featured listings */} +
+

{t('settingsUi.proSupport.yourFeatured')}

+ {featured.length === 0 ? ( +

{t('settingsUi.proSupport.noneFeatured')}

+ ) : ( +
+ {featured.map((p) => ( + + {p.title} + {t('settingsUi.proSupport.featuredUntil', { date: fmt(p.featuredUntil!) })} + + ))} +
+ )} +
+ + {/* Free paths */} +
+

{t('settingsUi.proSupport.freeTitle')}

+

{t('settingsUi.proSupport.freeBody')}

+
+
+ + +
+ ); +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index dca6179..fd94398 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -4307,6 +4307,29 @@ socialUi: { }, settingsUi: { + proSupport: { + title: 'Pro & Support', + subtitle: 'Optional extras that keep SkateSpots free', + prepaidTitle: 'Prepaid, not a subscription', + prepaidBody: 'Every purchase is a one-off Bitcoin/Lightning payment — no account, no recurring charge, no custody. Paying again extends your time.', + sasquatchTitle: 'Sasquatch Pro', + sasquatchBenefit: 'Unlocks the personal AI assistant — it can read your own Stash, crews and listings — and helps fund the project.', + active: 'Active', + activeUntil: 'Active until {{date}}', + daysLeft: '{{count}} days left', + notActive: 'Not active', + goPro: 'Go Pro', + extend: 'Extend Pro', + day: 'day', + featuredTitle: 'Featured placement', + featuredBenefit: 'Push a listing to the top with a Pro badge — for Lab profiles, events, shops and tours.', + featuredHow: 'Duration tiers apply to every type; longer commitments cost less per day. You feature a listing when you create or edit it.', + featuredUntil: 'Until {{date}}', + yourFeatured: 'Your featured listings', + noneFeatured: 'No active featured placements.', + freeTitle: 'Free paths', + freeBody: 'The AI FAQ and local search are always free, and you can publish a marketplace listing with a free proof-of-work instead of the sats fee.', + }, notFound: { seoTitle: '404 - Page Not Found', seoDescription: 'The page you are looking for could not be found. Return to the home page to continue browsing.', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 23bc589..669a2bc 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -4307,6 +4307,29 @@ socialUi: { }, settingsUi: { + proSupport: { + title: 'Pro y apoyo', + subtitle: 'Extras opcionales que mantienen SkateSpots gratis', + prepaidTitle: 'Pago por adelantado, no suscripción', + prepaidBody: 'Cada compra es un pago único en Bitcoin/Lightning — sin cuenta, sin cargo recurrente y sin custodia. Pagar de nuevo amplía tu tiempo.', + sasquatchTitle: 'Sasquatch Pro', + sasquatchBenefit: 'Desbloquea el asistente personal con IA — puede leer tu Stash, crews y anuncios — y ayuda a financiar el proyecto.', + active: 'Activo', + activeUntil: 'Activo hasta {{date}}', + daysLeft: 'Quedan {{count}} días', + notActive: 'No activo', + goPro: 'Hazte Pro', + extend: 'Ampliar Pro', + day: 'día', + featuredTitle: 'Posición destacada', + featuredBenefit: 'Lleva un anuncio arriba con un distintivo Pro — para perfiles del Lab, eventos, tiendas y tours.', + featuredHow: 'Los tramos de duración se aplican a cada tipo; los compromisos más largos cuestan menos por día. Destacas un anuncio al crearlo o editarlo.', + featuredUntil: 'Hasta {{date}}', + yourFeatured: 'Tus anuncios destacados', + noneFeatured: 'No hay destacados activos.', + freeTitle: 'Opciones gratis', + freeBody: 'El FAQ de IA y la búsqueda local son siempre gratis, y puedes publicar un anuncio con prueba de trabajo en vez de la tarifa en sats.', + }, notFound: { seoTitle: '404 - Página no encontrada', seoDescription: 'La página que buscas no se pudo encontrar. Vuelve a la página de inicio para seguir navegando.', diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 64a51c7..24cc7b9 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -13,6 +13,7 @@ import { RelatrSettings } from '@/components/RelatrSettings'; import { CVMHealthCheck } from '@/components/CVMHealthCheck'; import { SasquatchAiStatusCard } from '@/components/SasquatchAiStatusCard'; import { GrantProCard } from '@/components/GrantProCard'; +import { ProSupportScreen } from '@/components/ProSupportScreen'; import { AdminReviewCard } from '@/components/AdminReviewCard'; import { RegionBackfillCard } from '@/components/RegionBackfillCard'; import { NotificationSettingsCard } from '@/components/NotificationSettingsCard'; @@ -34,7 +35,7 @@ import { cn } from '@/lib/utils'; import { NIP05_PROVIDERS } from '@/lib/nip05Providers'; import { SKATE_NIP05_DOMAIN } from '@/lib/skateNip05'; import { useTranslation } from '@/contexts/language'; -import { User, Radio, Wallet, Wrench, RotateCcw, Shield, ExternalLink, Languages, RefreshCw, AlertCircle, Database, Package, ChevronRight, ChevronLeft, Smartphone, Download, Zap, WifiOff, Bell, Server } from 'lucide-react'; +import { User, Radio, Wallet, Wrench, RotateCcw, Shield, ExternalLink, Languages, RefreshCw, AlertCircle, Database, Package, ChevronRight, ChevronLeft, Smartphone, Download, Zap, WifiOff, Bell, Server, Crown } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -52,7 +53,8 @@ type Section = | 'stash' | 'notifications' | 'coordinator' - | 'advanced'; + | 'advanced' + | 'pro'; // ─── Back header ────────────────────────────────────────────────────────────── @@ -818,6 +820,14 @@ function SettingsMenu({ onNavigate }: { onNavigate: (s: Section) => void }) { iconBg="bg-pink-500/15" onClick={() => onNavigate('appearance')} /> + onNavigate('pro')} + /> } {section === 'appearance' && } + {section === 'pro' && } {section === 'wallet' && } {section === 'notifications' && (