diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ad2fbd2..2eb39a4 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -13,19 +13,18 @@ import { useTranslation } from '@/contexts/LanguageContext'; import { useQueryClient } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { NSchema as n } from '@nostrify/nostrify'; -import { useState } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { LoginArea } from '@/components/auth/LoginArea'; import { Button } from '@/components/ui/button'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; import { toast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; +import QRCode from 'qrcode'; import { User, - Palette, Radio, Wallet, - Settings, Wrench, RotateCcw, Shield, @@ -37,6 +36,12 @@ import { Package, ChevronRight, ChevronLeft, + Network, + Copy, + Check, + Smartphone, + Download, + Globe, } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -51,6 +56,7 @@ type Section = | 'language' | 'appearance' | 'relays' + | 'crewnetwork' | 'wallet' | 'stash' | 'advanced'; @@ -393,6 +399,247 @@ 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'; + +function CrewNetworkScreen({ onBack }: { onBack: () => void }) { + const { user, metadata } = useCurrentUser(); + const canvasRef = useRef(null); + const [copied, setCopied] = useState(false); + const [qrReady, setQrReady] = useState(false); + + // Build the join-request deep link using the user's npub as device identity. + // Format: nvpn://join-request?pubkey=&name= + // The nostr-vpn app uses the Nostr pubkey as stable device identity. + 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 { + await QRCode.toCanvas(canvasRef.current, joinUri, { + width: 220, + margin: 2, + color: { dark: '#000000', light: '#ffffff' }, + errorCorrectionLevel: 'M', + }); + setQrReady(true); + } catch { + // silently ignore QR render errors + } + }, [joinUri]); + + useEffect(() => { + renderQR(); + }, [renderQR]); + + const handleCopy = async () => { + if (!joinUri) return; + try { + await navigator.clipboard.writeText(joinUri); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + toast({ title: 'Copied!', description: 'Join request link copied to clipboard.' }); + } catch { + toast({ title: 'Copy failed', description: 'Could not copy to clipboard.', variant: 'destructive' }); + } + }; + + const handleOpenApp = () => { + // Try to open the native app via deep link; if not installed, fall through to App Store + const link = document.createElement('a'); + link.href = joinUri; + link.click(); + }; + + const platforms = [ + { label: 'iOS App Store', icon: Smartphone, url: NOSTR_VPN_APP_STORE, color: 'text-blue-400', bg: 'bg-blue-500/15', badge: 'Free' }, + { label: 'iOS TestFlight', icon: Smartphone, url: NOSTR_VPN_TESTFLIGHT, color: 'text-indigo-400', bg: 'bg-indigo-500/15', badge: 'Beta' }, + { label: 'macOS / Linux / Windows', icon: Download, url: NOSTR_VPN_GITHUB, color: 'text-green-400', bg: 'bg-green-500/15', badge: 'Open source' }, + ]; + + return ( +
+ +
+ + {/* Hero explainer */} +
+
+
+ +
+
+

Private Mesh VPN

+

+ nostr-vpn turns your Nostr keys into a private mesh network for your crew. + No accounts, no servers — your key is your identity. Encrypted peer-to-peer tunnels between skaters. +

+
+
+
+ + {/* QR + join request — only if logged in */} + {user ? ( +
+

+ Your Join Request +

+
+ {/* QR code canvas */} +
+ + {!qrReady && ( +
+
+
+ )} +
+ +
+

+ Show this QR to an admin in the nostr-vpn app. + They scan it, approve you, and you're on the mesh. +

+ + {/* URI preview */} +
+

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

+
+ + {/* Actions */} +
+ + +
+
+
+
+ ) : ( +
+ +

Login required

+

+ Log in with your Nostr keys to generate a join request for your crew's mesh network. +

+ +
+ )} + + {/* How it works */} +
+

+ How It Works +

+
+ {[ + { n: '1', text: 'Install nostr-vpn on your device (free, open source)' }, + { n: '2', text: 'Show your QR code above to a crew admin' }, + { n: '3', text: 'Admin approves — you get a signed roster back' }, + { n: '4', text: 'Encrypted mesh tunnel connects you to your crew' }, + ].map(({ n, text }) => ( +
+ + {n} + +

{text}

+
+ ))} +
+
+ + {/* Download links */} +
+

+ Get nostr-vpn +

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

{label}

+
+ + {badge} + + +
+ ))} +
+
+ + {/* What skaters use this for */} +
+

Use Cases

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

{label}

+
+ ))} +
+
+ + {/* Learn more */} + +
+
+ +
+
+

nostr-vpn on GitHub

+

Source code, docs, protocol spec

+
+
+ +
+ +
+
+ ); +} + // ─── Main settings menu ─────────────────────────────────────────────────────── function SettingsMenu({ onNavigate }: { onNavigate: (s: Section) => void }) { @@ -454,6 +701,15 @@ 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' && }