diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index ef84a5f..a7b192f 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -438,17 +438,31 @@ const STATIC_FAQ: Record = { 'what relay': "SkateSpots uses: πŸ›Ή goskatespots.nostr1.com (primary, allowlisted for quality) + relay.ditto.pub (global) + purplepag.es (profile aggregator). Your data is replicated across all of them for redundancy.", 'trip planner': "Tell me a city and I'll build a day-by-day skate trip πŸ—ΊοΈ β€” spots, events, coaches and local crews, weather-aware (rainy day = crew tip for covered spots). Try 'plan me 3 days in Lisbon'. I can save it all to a Collection, and hand you off to flights/stays on the real sites (no KYC, no middleman). πŸ›Ή", 'plan a trip': "Just say 'plan me 3 days in Barcelona' (or any city) and I'll map out spots, sessions, coaches and local crews for each day. Save it as a Collection and go shred. πŸ—ΊοΈπŸ›Ή", + // ── Sasquatch Pro β€” answered locally so it never depends on the AI brain ── + 'sasquatch pro': "**Sasquatch Pro** 🦧⚑ is a Lightning supporter pass that unlocks the personal side of me:\n\nβ€’ 🧠 **Personal assistant** β€” I know your Stash, crews, spots & listings\nβ€’ ✨ **Action drafts** β€” list gear, post a spot status, plan trips (you always confirm before anything publishes)\nβ€’ πŸš€ **Priority AI** β€” better model, higher limits, longer memory\nβ€’ 🌀️ **Proactive nudges** β€” weather + spot tips (opt-in)\nβ€’ πŸ… **Supporter badge** + Pro-only skins\n\nFree Sasquatch stays free: public spot/event/coach/crew search, the glossary & trip planning all work without Pro. Pro just adds the *personal* stuff and keeps SkateSpots ad-free. Tap **⚑ Go Pro** up top. Tiers: 7 days ~2k sats, 1 month ~5k, 3 months ~12k, 1 year ~36k.", + 'pro perks': "Pro perks 🦧⚑: personal assistant (reads your Stash/crews/spots/listings), action drafts (list gear, post status, plan trips β€” you confirm), priority AI model with higher limits & longer memory, opt-in weather/spot nudges, and a supporter badge + Pro skins. Free stays free β€” Pro adds the personal layer & keeps the app ad-free. Tap **⚑ Go Pro** up top.", + 'pro version': "The Pro version of me 🦧⚑ adds a **personal assistant**: I can read your Stash, crews, spots & listings, and draft actions for you (list gear, post a status, plan a trip β€” you always confirm first). Plus a priority AI model, higher limits, longer memory, opt-in weather nudges, and a supporter badge. Free Sasquatch (spot/event/coach/crew search, glossary, trip planner) stays free. Tap **⚑ Go Pro** up top.", + 'perks': "Pro perks 🦧⚑: personal assistant (your Stash/crews/spots/listings), action drafts you confirm, priority AI + higher limits + longer memory, opt-in weather/spot nudges, and a supporter badge + Pro skins. Tap **⚑ Go Pro** up top.", + 'go pro': "Tap the **⚑ Go Pro** button up top to open the upgrade sheet. Pro unlocks my personal assistant (Stash/crews/spots/listings), action drafts, priority AI and a supporter badge. Pay with Lightning β€” 7 days ~2k sats up to 1 year ~36k sats. Free features stay free. 🦧", + 'pro pricing': "Pro tiers (pay with Lightning ⚑): **7 days** ~2,000 sats (~$1.20), **1 month** ~5,000 sats (~$3), **3 months** ~12,000 sats (~$7, 20% off), **1 year** ~36,000 sats (~$21, best value). Tap **⚑ Go Pro** up top.", + 'how much is pro': "Pro tiers: 7 days ~2,000 sats, 1 month ~5,000 sats, 3 months ~12,000 sats (20% off), 1 year ~36,000 sats (best value). Pay with Lightning ⚑. Tap **⚑ Go Pro** up top.", + 'upgrade': "To upgrade to Sasquatch Pro, tap the **⚑ Go Pro** button up top. Pay with Lightning (7 days ~2k sats up to 1 year ~36k). Pro unlocks my personal assistant, action drafts, priority AI and a supporter badge. 🦧⚑", }; function getStaticAnswer(question: string): string | null { const q = question.toLowerCase().trim(); // Exact match first (for greetings like "hi", "hey") if (STATIC_FAQ[q]) return STATIC_FAQ[q]; - // Then partial match + + // Partial match β€” prefer the LONGEST matching key so specific phrases + // ("sasquatch pro", "pro pricing") win over broad ones ("pro", "perks"). + let best: { key: string; answer: string } | null = null; for (const [key, answer] of Object.entries(STATIC_FAQ)) { - if (q.includes(key)) return answer; + if (q.includes(key) && (!best || key.length > best.key.length)) { + best = { key, answer }; + } } - return null; + return best?.answer ?? null; } // ─── CVM connection status indicator ───────────────────────────────────────── diff --git a/src/components/SasquatchProDialog.tsx b/src/components/SasquatchProDialog.tsx index e97806a..39df138 100644 --- a/src/components/SasquatchProDialog.tsx +++ b/src/components/SasquatchProDialog.tsx @@ -75,7 +75,7 @@ export function SasquatchProDialog({ // Toast + close on success. useEffect(() => { if (purchase.state === 'paid') { - toast({ title: 'Welcome to Sasquatch Pro! 🦧⚑', description: 'Your supporter pass is active. Thanks for keeping the streets free.' }); + toast({ title: 'Welcome to Sasquatch Pro! 🦧⚑', description: 'Your pass is active β€” a receipt just landed in your DMs. Thanks for keeping the streets free.' }); const t = setTimeout(() => { onOpenChange(false); purchase.reset(); }, 1400); return () => clearTimeout(t); } diff --git a/src/hooks/usePurchasePro.ts b/src/hooks/usePurchasePro.ts index 836fc93..c91cb9a 100644 --- a/src/hooks/usePurchasePro.ts +++ b/src/hooks/usePurchasePro.ts @@ -21,6 +21,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNWC } from '@/hooks/useNWCContext'; import { useWallet } from '@/hooks/useWallet'; import { useAppContext } from '@/hooks/useAppContext'; +import { useSendDirectMessage } from '@/hooks/useDirectMessages'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import { getZapProviderPubkey, @@ -47,9 +48,45 @@ export function usePurchasePro() { const { getActiveConnection, sendPayment } = useNWC(); const { webln } = useWallet(); const { config } = useAppContext(); + const { mutateAsync: sendDM } = useSendDirectMessage(); const [result, setResult] = useState({ invoice: null, state: 'idle', error: null }); + /** + * Send the buyer an encrypted (NIP-17) confirmation to their own inbox β€” a + * durable, private receipt of what they bought. Best-effort: a failure here + * never blocks the purchase (they already paid + got the on-relay receipt). + */ + const sendConfirmationDM = useCallback( + async (tier: ProTier, expiry: number) => { + if (!user?.pubkey || !user.signer?.nip44) return; + const until = new Date(expiry * 1000).toLocaleDateString(undefined, { + year: 'numeric', month: 'long', day: 'numeric', + }); + const content = [ + `🦧⚑ Sasquatch Pro β€” you're in!`, + ``, + `Pass: ${tier.label} (${tier.sats.toLocaleString()} sats)`, + `Active until: ${until}`, + ``, + `What you unlocked:`, + `β€’ Personal assistant β€” I read your Stash, crews, spots & listings`, + `β€’ Action drafts β€” list gear, post a spot status, plan trips (you confirm)`, + `β€’ Priority AI model, higher limits & longer memory`, + `β€’ Opt-in weather + spot nudges`, + `β€’ ⚑ Supporter badge + Pro-only skins`, + ``, + `Thanks for keeping SkateSpots free & ad-free. Now go shred. πŸ›Ή`, + ].join('\n'); + try { + await sendDM({ recipients: user.pubkey, content, subject: 'Sasquatch Pro receipt' }); + } catch { + /* best-effort β€” don't fail the purchase over a receipt DM */ + } + }, + [user, sendDM], + ); + /** Publish the self-signed fallback receipt for a paid tier. */ const publishReceipt = useCallback( async (tier: ProTier, expiry: number) => { @@ -125,6 +162,7 @@ export function usePurchasePro() { try { await sendPayment(nwc, invoice); await publishReceipt(tier, expiry); + await sendConfirmationDM(tier, expiry); setResult({ invoice: null, state: 'paid', error: null }); return; } catch { @@ -137,6 +175,7 @@ export function usePurchasePro() { if (typeof webln.enable === 'function') await webln.enable(); await webln.sendPayment(invoice); await publishReceipt(tier, expiry); + await sendConfirmationDM(tier, expiry); setResult({ invoice: null, state: 'paid', error: null }); return; } catch { @@ -155,7 +194,7 @@ export function usePurchasePro() { }); } }, - [user, config, getActiveConnection, sendPayment, webln, publishReceipt], + [user, config, getActiveConnection, sendPayment, webln, publishReceipt, sendConfirmationDM], ); /** Called after a manual (QR) payment to record the pass locally. */ @@ -164,6 +203,7 @@ export function usePurchasePro() { const expiry = Math.floor(Date.now() / 1000) + tier.days * DAY_SECONDS; try { await publishReceipt(tier, expiry); + await sendConfirmationDM(tier, expiry); setResult({ invoice: null, state: 'paid', error: null }); } catch (err) { setResult({ @@ -173,7 +213,7 @@ export function usePurchasePro() { }); } }, - [publishReceipt], + [publishReceipt, sendConfirmationDM], ); const reset = useCallback(() => setResult({ invoice: null, state: 'idle', error: null }), []); diff --git a/src/lib/sasquatchBrain.ts b/src/lib/sasquatchBrain.ts index f1a33cc..485457a 100644 --- a/src/lib/sasquatchBrain.ts +++ b/src/lib/sasquatchBrain.ts @@ -14,6 +14,9 @@ const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'; // rate-limited (429) we back off briefly then move to the next model. export const FREE_MODELS = [ 'meta-llama/llama-3.3-70b-instruct:free', + 'deepseek/deepseek-chat-v3-0324:free', + 'google/gemini-2.0-flash-exp:free', + 'qwen/qwen-2.5-72b-instruct:free', 'google/gemma-2-9b-it:free', 'mistralai/mistral-7b-instruct:free', 'qwen/qwen-2.5-7b-instruct:free',