SkateSpots/src/components/SasquatchProDialog.tsx
SkateSpots 3e6354995a i18n(es): batch 3 part 1 — routing UI, Sasquatch panel, collections, splash
- routeUi.* (123 keys): RouteDialog (incl. follow-mode banner, transit time
  picker + mode filters, itinerary legs), StopDepartures, TripItineraryCard.
- sasquatchUi.* (81 keys): SasquatchGuide, SasquatchProDialog,
  SasquatchActionCard, SasquatchAiStatusCard. LLM prompts + chat bodies
  deliberately left English.
- collectionUi.* / bootUi.*: AddToCollectionDialog example placeholders,
  SplashScreen rotating status lines + aria labels.
- New src/i18n/batch3/* fragment files, spread into en/es so parallel passes
  never edit the big literal.
2026-10-02 15:15:06 +07:00

257 lines
10 KiB
TypeScript

/**
* SasquatchProDialog — the "Upgrade to Sasquatch Pro" sheet.
*
* Shows the supporter tiers, the perk list, and handles Lightning payment
* (auto via NWC/WebLN, or a QR fallback). Framed as supporting a free,
* no-ads, community-funded app — Pro is the thank-you.
*/
import { useEffect, useState } from 'react';
import QRCode from 'qrcode';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useSasquatchPro } from '@/hooks/useSasquatchPro';
import { usePurchasePro } from '@/hooks/usePurchasePro';
import { useExchangeRate } from '@/hooks/useExchangeRate';
import { useToast } from '@/hooks/useToast';
import { PRO_TIERS, PRO_LIGHTNING_ADDRESS, type ProTier } from '@/lib/sasquatchPro';
import { useTranslation } from '@/contexts/language';
import {
Zap,
Check,
Loader2,
Copy,
Brain,
Wand2,
Rocket,
BadgeCheck,
} from 'lucide-react';
const PERKS: Array<{ icon: React.ComponentType<{ className?: string }>; textKey: string }> = [
{ icon: Brain, textKey: 'sasquatchUi.perk1' },
{ icon: Wand2, textKey: 'sasquatchUi.perk2' },
{ icon: Rocket, textKey: 'sasquatchUi.perk3' },
{ icon: BadgeCheck, textKey: 'sasquatchUi.perk4' },
];
export function SasquatchProDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
}) {
const { user } = useCurrentUser();
const { t } = useTranslation();
const { isPro, expiresAt } = useSasquatchPro();
const { toast } = useToast();
const purchase = usePurchasePro();
const [selected, setSelected] = useState<ProTier>(
PRO_TIERS.find((t) => t.popular) ?? PRO_TIERS[1],
);
const [qr, setQr] = useState<string | null>(null);
// Render a QR whenever we fall back to manual payment.
useEffect(() => {
let active = true;
if (purchase.invoice) {
QRCode.toDataURL(purchase.invoice.toUpperCase(), { width: 280, margin: 1 })
.then((url) => { if (active) setQr(url); })
.catch(() => setQr(null));
} else {
setQr(null);
}
return () => { active = false; };
}, [purchase.invoice]);
// Toast + close on success.
useEffect(() => {
if (purchase.state === 'paid') {
toast({ title: t('sasquatchUi.welcomeTitle'), description: t('sasquatchUi.welcomeDesc') });
const timer = setTimeout(() => { onOpenChange(false); purchase.reset(); }, 1400);
return () => clearTimeout(timer);
}
}, [purchase.state, toast, onOpenChange, purchase, t]);
const copyInvoice = () => {
if (purchase.invoice) {
navigator.clipboard.writeText(purchase.invoice);
toast({ title: t('sasquatchUi.invoiceCopied') });
}
};
const busy = purchase.state === 'preparing';
const manual = purchase.state === 'awaiting_payment' && !!purchase.invoice;
return (
<Dialog open={open} onOpenChange={(v) => { onOpenChange(v); if (!v) purchase.reset(); }}>
<DialogContent className="max-w-md bg-black border-red-500/30 text-white max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl font-black">
<span className="text-2xl">🦧</span> Sasquatch <span className="text-red-500">Pro</span>
</DialogTitle>
<DialogDescription className="text-gray-400">
{t('sasquatchUi.dialogDesc')}
</DialogDescription>
</DialogHeader>
{isPro ? (
// ── Already Pro ─────────────────────────────────────────────────
<div className="space-y-4 py-2">
<div className="rounded-2xl border border-[#ccff00]/30 bg-[#ccff00]/10 p-4 text-center">
<BadgeCheck className="mx-auto mb-2 h-8 w-8 text-[#ccff00]" />
<p className="font-black text-[#ccff00]">{t('sasquatchUi.proSupporter')}</p>
{expiresAt && (
<p className="mt-1 text-xs text-gray-300">
{t('sasquatchUi.activeUntil', { date: new Date(expiresAt * 1000).toLocaleDateString() })}
</p>
)}
</div>
<div className="space-y-2">
{PERKS.map((p) => (
<div key={p.textKey} className="flex items-start gap-2 text-sm text-gray-300">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-[#ccff00]" />
{t(p.textKey)}
</div>
))}
</div>
<p className="text-center text-xs text-gray-500">
{t('sasquatchUi.extendPrompt')}
</p>
<TierPicker selected={selected} onSelect={setSelected} />
<PayButton tier={selected} busy={busy} onPay={() => purchase.purchase(selected)} extend />
</div>
) : manual ? (
// ── Manual QR payment ───────────────────────────────────────────
<div className="space-y-4 py-2 text-center">
<p className="text-sm text-gray-300">
{t('sasquatchUi.scanToPayPre')}{' '}
<span className="font-black text-white">{t('sasquatchUi.scanToPayAmount', { sats: selected.sats.toLocaleString() })}</span>{' '}
{t('sasquatchUi.scanToPayPost', { label: selected.label })}
</p>
{qr && <img src={qr} alt={t('sasquatchUi.invoiceQrAlt')} className="mx-auto rounded-xl bg-white p-2" width={240} height={240} />}
<div className="flex gap-2">
<Button variant="outline" className="flex-1 gap-2 border-white/15" onClick={copyInvoice}>
<Copy className="h-4 w-4" /> {t('sasquatchUi.copyInvoice')}
</Button>
<Button
className="flex-1 gap-2 bg-[#ccff00] text-black hover:bg-[#d4ff1a] font-bold"
onClick={() => purchase.confirmManual(selected)}
>
<Check className="h-4 w-4" /> {t('sasquatchUi.ivePaid')}
</Button>
</div>
<p className="text-[11px] text-gray-400">
{t('sasquatchUi.paysTo')}{' '}
<span className="font-mono text-gray-200">⚡ {PRO_LIGHTNING_ADDRESS}</span>
</p>
<p className="text-[11px] text-gray-500">
{t('sasquatchUi.ivePaidHint')}
</p>
</div>
) : (
// ── Purchase flow ───────────────────────────────────────────────
<div className="space-y-4 py-2">
<div className="space-y-2">
{PERKS.map((p) => (
<div key={p.textKey} className="flex items-start gap-2.5 text-sm text-gray-200">
<p.icon className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
{t(p.textKey)}
</div>
))}
</div>
<div className="rounded-xl border border-white/10 bg-white/[0.03] p-3 text-xs text-gray-400">
{t('sasquatchUi.freeStaysPre')}<span className="text-white font-semibold">{t('sasquatchUi.freeStaysEmphasis')}</span>{t('sasquatchUi.freeStaysPost')}
</div>
<TierPicker selected={selected} onSelect={setSelected} />
{!user && (
<p className="text-center text-xs text-amber-400">{t('sasquatchUi.loginFirst')}</p>
)}
{purchase.error && (
<p className="text-center text-xs text-red-400">{purchase.error}</p>
)}
<PayButton tier={selected} busy={busy} onPay={() => purchase.purchase(selected)} disabled={!user} />
<p className="text-center text-[11px] text-gray-500">
{t('sasquatchUi.paysTo')} <span className="font-mono text-gray-300">⚡ {PRO_LIGHTNING_ADDRESS}</span> {t('sasquatchUi.overLightning')}
</p>
</div>
)}
</DialogContent>
</Dialog>
);
}
function TierPicker({ selected, onSelect }: { selected: ProTier; onSelect: (t: ProTier) => void }) {
// Live BTC/USD so the fiat hint never goes stale (it used to be hardcoded).
const { data: btcUsd } = useExchangeRate();
const { t } = useTranslation();
return (
<div className="grid grid-cols-2 gap-2">
{PRO_TIERS.map((tier) => {
const active = tier.id === selected.id;
const usd = btcUsd ? `~$${((tier.sats / 1e8) * btcUsd).toFixed(2)}` : '';
return (
<button
key={tier.id}
onClick={() => onSelect(tier)}
className={cn(
'relative rounded-xl border p-3 text-left transition-all',
active
? 'border-red-500 bg-red-500/15'
: 'border-white/10 bg-white/[0.02] hover:border-white/25',
)}
>
{tier.popular && (
<span className="absolute -top-2 right-2 rounded-full bg-red-500 px-1.5 py-0.5 text-[8px] font-black uppercase text-white">
{t('sasquatchUi.popular')}
</span>
)}
<p className="text-sm font-black text-white">{tier.label}</p>
<p className="text-lg font-black text-red-400">{tier.sats.toLocaleString()}<span className="ml-0.5 text-xs text-gray-500">{t('sasquatchUi.sats')}</span></p>
<p className="text-[10px] text-gray-500">{[usd, tier.badge].filter(Boolean).join(' · ')}</p>
</button>
);
})}
</div>
);
}
function PayButton({
tier,
busy,
disabled,
extend,
onPay,
}: {
tier: ProTier;
busy: boolean;
disabled?: boolean;
extend?: boolean;
onPay: () => void;
}) {
const { t } = useTranslation();
return (
<Button
className="w-full gap-2 bg-red-500 hover:bg-red-600 font-black uppercase tracking-wide h-12"
onClick={onPay}
disabled={busy || disabled}
>
{busy ? <Loader2 className="h-5 w-5 animate-spin" /> : <Zap className="h-5 w-5" />}
{extend ? t('sasquatchUi.extend') : t('sasquatchUi.goPro')} · {tier.sats.toLocaleString()} {t('sasquatchUi.sats')}
</Button>
);
}