diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index c9b9b9a..807113f 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -16,7 +16,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { NSchema as n } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useState } from 'react'; import { LoginArea } from '@/components/auth/LoginArea'; import { Button } from '@/components/ui/button'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -25,8 +25,7 @@ import { useCitrine } from '@/hooks/useCitrine'; import { toast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; import { NIP05_PROVIDERS } from '@/lib/nip05Providers'; -import QRCode from 'qrcode'; -import { User, Radio, Wallet, Wrench, RotateCcw, Shield, ExternalLink, Languages, RefreshCw, AlertCircle, Database, Package, ChevronRight, ChevronLeft, Network, Copy, Check, Smartphone, Download, Globe, Zap, WifiOff } from 'lucide-react'; +import { User, Radio, Wallet, Wrench, RotateCcw, Shield, ExternalLink, Languages, RefreshCw, AlertCircle, Database, Package, ChevronRight, ChevronLeft, Smartphone, Download, Zap, WifiOff } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -40,7 +39,6 @@ type Section = | 'language' | 'appearance' | 'relays' - | 'crewnetwork' | 'wallet' | 'stash' | 'advanced'; @@ -659,358 +657,6 @@ function WalletScreen({ onBack }: { onBack: () => void }) { ); } -// ─── Crew Network sub-screen ────────────────────────────────────────────────── - -const NOSTR_VPN_APP_STORE = 'https://apps.apple.com/app/nostr-vpn/id6785410348'; -const NOSTR_VPN_GITHUB = 'https://github.com/mmalmi/nostr-vpn/releases/latest'; -const NOSTR_VPN_TESTFLIGHT = 'https://testflight.apple.com/join/58sg4agv'; -const NOSTR_VPN_ANDROID = 'https://github.com/mmalmi/nostr-vpn/releases/latest'; - -/** Detect the user's OS/platform to surface the most relevant download first. */ -function detectPlatform(): 'ios' | 'android' | 'desktop' { - const ua = navigator.userAgent; - if (/android/i.test(ua)) return 'android'; - if (/iphone|ipad|ipod/i.test(ua)) return 'ios'; - return 'desktop'; -} - -const ALL_PLATFORMS = [ - { id: 'ios', label: 'iOS App Store', sublabel: 'iPhone & iPad', icon: Smartphone, url: NOSTR_VPN_APP_STORE, color: 'text-blue-400', bg: 'bg-blue-500/15', badge: 'Free' }, - { id: 'testflight',label: 'iOS TestFlight', sublabel: 'Beta builds', icon: Smartphone, url: NOSTR_VPN_TESTFLIGHT, color: 'text-indigo-400', bg: 'bg-indigo-500/15', badge: 'Beta' }, - { id: 'android', label: 'Android APK', sublabel: 'arm64 devices', icon: Smartphone, url: NOSTR_VPN_ANDROID, color: 'text-green-400', bg: 'bg-green-500/15', badge: 'APK' }, - { id: 'desktop', label: 'macOS / Linux / Windows',sublabel: 'Native desktop apps', icon: Download, url: NOSTR_VPN_GITHUB, color: 'text-amber-400', bg: 'bg-amber-500/15', badge: 'Open source' }, -]; - -function CrewNetworkScreen({ onBack }: { onBack: () => void }) { - const { user, metadata } = useCurrentUser(); - const canvasRef = useRef(null); - const [copied, setCopied] = useState(false); - const [qrReady, setQrReady] = useState(false); - const [qrError, setQrError] = useState(false); - const [openingApp, setOpeningApp] = useState(false); - - // Detect platform once — sort most-relevant download to the top. - const platform = detectPlatform(); - const platforms = [...ALL_PLATFORMS].sort((a) => { - if (platform === 'ios' && (a.id === 'ios' || a.id === 'testflight')) return -1; - if (platform === 'android' && a.id === 'android') return -1; - if (platform === 'desktop' && a.id === 'desktop') return -1; - return 0; - }); - - // Build the join-request deep link using the user's npub as device identity. - // Format: nvpn://join-request?pubkey=&name=&relay= - const joinUri = user - ? `nvpn://join-request?pubkey=${user.pubkey}&name=${encodeURIComponent(metadata?.name ?? 'Skater')}&relay=${encodeURIComponent('wss://goskatespots.nostr1.com')}` - : ''; - - const renderQR = useCallback(async () => { - if (!canvasRef.current || !joinUri) return; - try { - setQrError(false); - await QRCode.toCanvas(canvasRef.current, joinUri, { - width: 220, - margin: 2, - color: { dark: '#000000', light: '#ffffff' }, - errorCorrectionLevel: 'M', - }); - setQrReady(true); - } catch { - setQrError(true); - } - }, [joinUri]); - - useEffect(() => { - renderQR(); - }, [renderQR]); - - const handleCopy = async () => { - if (!joinUri) return; - try { - await navigator.clipboard.writeText(joinUri); - setCopied(true); - setTimeout(() => setCopied(false), 2500); - toast({ title: 'Copied!', description: 'Join request link copied to clipboard.' }); - } catch { - toast({ title: 'Copy failed', description: 'Could not access clipboard.', variant: 'destructive' }); - } - }; - - // Save the QR canvas as a PNG the user can share. - const handleDownloadQR = () => { - if (!canvasRef.current || !qrReady) return; - const link = document.createElement('a'); - link.download = 'nostr-vpn-join-request.png'; - link.href = canvasRef.current.toDataURL('image/png'); - link.click(); - }; - - // Open the deep link on the device. On iOS Safari, window.location.href inside - // a user-gesture handler is the only reliable way to fire custom schemes. - // After 2 s with no visibility change, assume the app isn't installed and - // redirect to the correct store page. - const handleOpenApp = () => { - if (!joinUri) return; - setOpeningApp(true); - - const fallbackUrl = platform === 'android' ? NOSTR_VPN_ANDROID : NOSTR_VPN_APP_STORE; - - const timeout = setTimeout(() => { - // Still here → app not installed → go to store/release page - window.location.href = fallbackUrl; - }, 2000); - - // Cancel the fallback if the page loses focus (app opened successfully). - const cancel = () => { - clearTimeout(timeout); - setOpeningApp(false); - }; - window.addEventListener('blur', cancel, { once: true }); - document.addEventListener('visibilitychange', () => { - if (document.hidden) cancel(); - }, { once: true }); - - window.location.href = joinUri; - }; - - // Steps adapt: if the user is logged out, step 2 says "log in" instead of "show QR". - const steps = [ - { n: '1', text: 'Install nostr-vpn on your device (free, open source)' }, - { n: '2', text: user ? 'Show your QR code above to a crew admin' : 'Log in to SkateSpots to generate your personal QR' }, - { n: '3', text: 'Admin approves — you receive a cryptographically signed roster' }, - { n: '4', text: 'WireGuard-grade encrypted mesh tunnel connects you to your crew' }, - ]; - - return ( -
- -
- - {/* Hero explainer */} -
-
-
-
-
-

Private Encrypted Mesh

-

- nostr-vpn turns your Nostr keys into a private WireGuard mesh for your crew. - No accounts, no central server — your key is your identity. Your crew, your network. -

-
-
-
- - {/* QR + join request */} - {user ? ( -
-

- Your Join Request -

-
- - {/* QR canvas — fixed container prevents layout shift */} -
-
- {/* Loading spinner — shown until QR renders */} - {!qrReady && !qrError && ( -
-
-

Generating…

-
- )} - {/* Error state */} - {qrError && ( -
- -

QR failed to render

- -
- )} - -
-
- -
-

- Show this QR to an admin in the nostr-vpn app. - They scan → approve → you're on the encrypted mesh. -

- - {/* URI display — improved contrast, selectable */} -
-

Join URI

-

- {joinUri.length > 130 ? joinUri.slice(0, 130) + '…' : joinUri} -

-
- - {/* Actions row */} -
- - - -
- - {/* Deep link hint */} -

- "Open app" requires nostr-vpn installed. If not installed, you'll be redirected to the download page. -

-
-
-
- ) : ( -
-
- )} - - {/* How it works — steps adapt based on login state */} -
-

- How It Works -

-
    - {steps.map(({ n, text }) => ( -
  1. - -

    {text}

    -
  2. - ))} -
-
- - {/* Download links — platform-sorted */} -
-

- Get nostr-vpn -

-
- {platforms.map(({ id, label, sublabel, icon: Icon, url, color, bg, badge }) => ( - - -
-

{label}

-

{sublabel}

-
- - {badge} - -
- ))} -
-
- - {/* Use cases */} -
-

Why Skaters Use It

-
    - {[ - { emoji: '🔒', label: 'Private crew comms' }, - { emoji: '📍', label: 'Secret spot sharing' }, - { emoji: '🌍', label: 'Censorship bypass' }, - { emoji: '📶', label: 'Offline session mesh' }, - ].map(({ emoji, label }) => ( -
  • - -

    {label}

    -
  • - ))} -
-
- - {/* Learn more */} - -
- -
-

nostr-vpn on GitHub

-

Source code, docs, protocol spec

-
-
-
- -
-
- ); -} - // ─── Main settings menu ─────────────────────────────────────────────────────── function SettingsMenu({ onNavigate }: { onNavigate: (s: Section) => void }) { @@ -1072,15 +718,6 @@ function SettingsMenu({ onNavigate }: { onNavigate: (s: Section) => void }) { iconBg="bg-cyan-500/15" onClick={() => onNavigate('relays')} /> - onNavigate('crewnetwork')} - /> )} {section === 'relays' && } - {section === 'crewnetwork' && } {section === 'appearance' && } {section === 'wallet' && } {section === 'advanced' && }