SkateSpots/src/pages/SignersPage.tsx
SkateSpots 087f490acc refactor(layout): hoist Layout into the router so the chrome mounts once
- AppRouter wraps every route (except /amber-callback and the 404) in ONE
  <Route element={<Layout/>}> with an <Outlet/>; the chrome now persists across
  navigation instead of being torn down and rebuilt on every page.
- Layout: children optional (falls back to <Outlet/>); the map is full-bleed via
  a route check (isFullBleed) instead of a per-page noPadding prop.
- All 59 pages + CraftProfileForm/ErrorState/NewsletterIssueView now render
  fragments instead of <Layout> (structure-preserving), imports dropped.

Removes the whole remount class: chrome no longer re-runs its hooks per
navigation, Sasquatch chat state survives navigation, and the header can't
re-open Amber on every page change.
2026-09-27 13:41:42 +07:00

364 lines
14 KiB
TypeScript

import { useSeoMeta } from '@unhead/react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Smartphone, Shield, ExternalLink, Star, CheckCircle2, Info, Apple, Chrome } from 'lucide-react';
// Platform detection
const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
const isIOS = typeof navigator !== 'undefined' && /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isMobile = isAndroid || isIOS;
interface SignerApp {
name: string;
description: string;
features: string[];
url: string;
recommended?: boolean;
platforms: ('android' | 'ios' | 'web' | 'desktop')[];
protocol?: string;
}
const androidSigners: SignerApp[] = [
{
name: 'Amber',
description: 'Native Android signer focused on security and ease of use. The best choice for Android users.',
features: ['Native app', 'Multiple key support', 'Event signing', 'Secure key storage'],
url: 'https://github.com/greenart7c3/Amber',
recommended: true,
platforms: ['android'],
protocol: 'NIP-55',
},
{
name: 'Nsec.app',
description: 'Cross-platform remote signer that acts as a secure vault for your keys.',
features: ['Remote signing', 'Works with any client', 'Cross-device sync'],
url: 'https://nsec.app',
platforms: ['android', 'ios', 'web'],
protocol: 'NIP-46',
},
{
name: 'Nostria Signer',
description: 'Remote signer app with keys stored in secure device storage.',
features: ['Secure storage', 'Remote signing', 'Multi-key support'],
url: 'https://nostria.app',
platforms: ['android', 'ios'],
protocol: 'NIP-46',
},
{
name: 'Nowser',
description: 'Multi-auth signer supporting both browser extension and remote signing protocols.',
features: ['NIP-07 support', 'NIP-46 support', 'Flexible signing'],
url: 'https://nowser.app',
platforms: ['android', 'ios'],
protocol: 'NIP-07 / NIP-46',
},
];
const iosSigners: SignerApp[] = [
{
name: 'Nsec.app',
description: 'The best option for iOS. Use it as a remote signer with any Nostr client.',
features: ['Remote signing', 'Works with any client', 'Cross-device sync'],
url: 'https://nsec.app',
recommended: true,
platforms: ['android', 'ios', 'web'],
protocol: 'NIP-46',
},
{
name: 'Nowser (TestFlight)',
description: 'iOS signer available via TestFlight with signing capabilities similar to Android.',
features: ['NIP-07 support', 'NIP-46 support', 'Native iOS app'],
url: 'https://testflight.apple.com/join/nowser',
platforms: ['ios'],
protocol: 'NIP-07 / NIP-46',
},
{
name: 'Nostria Signer',
description: 'Remote signer with secure key storage on your device.',
features: ['Secure storage', 'Remote signing', 'Multi-key support'],
url: 'https://nostria.app',
platforms: ['android', 'ios'],
protocol: 'NIP-46',
},
];
const browserSigners: SignerApp[] = [
{
name: 'nos2x',
description: 'Simple and reliable browser extension for signing Nostr events.',
features: ['Lightweight', 'Easy to use', 'Chrome/Brave/Edge support'],
url: 'https://chrome.google.com/webstore/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp',
recommended: true,
platforms: ['web'],
protocol: 'NIP-07',
},
{
name: 'Alby',
description: 'Bitcoin Lightning wallet with built-in Nostr key management and WebLN.',
features: ['Lightning payments', 'Nostr signing', 'WebLN support', 'NWC support'],
url: 'https://getalby.com',
platforms: ['web', 'desktop'],
protocol: 'NIP-07',
},
{
name: 'Flamingo',
description: 'Nostr-focused browser extension with clean interface.',
features: ['Nostr signing', 'Clean UI', 'Privacy-focused'],
url: 'https://www.flamingowallet.com',
platforms: ['web'],
protocol: 'NIP-07',
},
{
name: 'Kiwi Browser + Extension',
description: 'On Android/GrapheneOS, use Kiwi Browser to load nos2x or Alby extensions.',
features: ['Extension support on mobile', 'Full browser experience', 'Works on GrapheneOS'],
url: 'https://kiwibrowser.com',
platforms: ['android'],
protocol: 'NIP-07',
},
];
function SignerCard({ signer }: { signer: SignerApp }) {
return (
<Card className={`bg-black/50 border-2 transition-all hover:shadow-lg ${
signer.recommended
? 'border-[#ccff00]/50 shadow-[0_0_15px_rgba(204,255,0,0.1)]'
: 'border-gray-700/50 hover:border-gray-600'
}`}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg font-bold flex items-center gap-2">
{signer.name}
{signer.recommended && (
<Badge className="bg-[#ccff00] text-black text-xs">
<Star className="h-3 w-3 mr-1" />
Recommended
</Badge>
)}
</CardTitle>
{signer.protocol && (
<Badge variant="outline" className="mt-1 text-xs text-gray-400 border-gray-600">
{signer.protocol}
</Badge>
)}
</div>
</div>
<CardDescription className="text-gray-400 mt-2">
{signer.description}
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap gap-1.5 mb-4">
{signer.features.map((feature) => (
<span
key={feature}
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-gray-800 text-gray-300"
>
<CheckCircle2 className="h-3 w-3 text-green-500" />
{feature}
</span>
))}
</div>
<Button
asChild
className={`w-full ${
signer.recommended
? 'bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
: 'bg-gray-800 hover:bg-gray-700'
}`}
>
<a href={signer.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4 mr-2" />
Get {signer.name}
</a>
</Button>
</CardContent>
</Card>
);
}
function PlatformSection({
icon: Icon,
title,
description,
signers,
highlighted,
}: {
icon: React.ComponentType<{ className?: string }>;
title: string;
description: string;
signers: SignerApp[];
highlighted?: boolean;
}) {
return (
<section className={`py-8 ${highlighted ? 'bg-[#ccff00]/5 -mx-4 px-4 rounded-2xl border border-[#ccff00]/20' : ''}`}>
<div className="flex items-center gap-3 mb-2">
<div className={`p-2 rounded-lg ${highlighted ? 'bg-[#ccff00]/20' : 'bg-gray-800'}`}>
<Icon className={`h-6 w-6 ${highlighted ? 'text-[#ccff00]' : 'text-gray-400'}`} />
</div>
<div>
<h2 className={`text-xl font-bold ${highlighted ? 'text-[#ccff00]' : 'text-white'}`}>
{title}
</h2>
{highlighted && (
<Badge className="bg-[#ccff00] text-black text-xs mt-1">
Your Platform
</Badge>
)}
</div>
</div>
<p className="text-gray-400 mb-6 ml-12">{description}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{signers.map((signer) => (
<SignerCard key={signer.name} signer={signer} />
))}
</div>
</section>
);
}
export default function SignersPage() {
useSeoMeta({
title: 'Signer Apps - Secure Your Nostr Keys | SkateSpots',
description: 'Learn about signer apps that keep your Nostr keys secure. Find the best option for Android, iOS, or desktop browsers.',
});
// Determine platform order - show user's platform first
const platformOrder = isAndroid
? ['android', 'ios', 'browser'] as const
: isIOS
? ['ios', 'android', 'browser'] as const
: ['browser', 'android', 'ios'] as const;
return (
<>
<div className="min-h-screen bg-black">
{/* Hero Section */}
<section className="relative py-12 md:py-16 border-b border-[#ff0040]/30">
<div className="absolute inset-0 bg-gradient-to-b from-[#ff0040]/10 to-transparent" />
<div className="container px-4 relative">
<div className="max-w-3xl mx-auto text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[#ff0040]/20 border border-[#ff0040]/50 mb-6">
<Shield className="h-5 w-5 text-[#ff0040]" />
<span className="text-[#ff0040] font-bold uppercase text-sm">Security Guide</span>
</div>
<h1 className="text-3xl md:text-5xl font-black uppercase mb-4" style={{
fontFamily: 'Impact, "Arial Black", sans-serif',
}}>
<span className="text-white">Protect Your</span>{' '}
<span className="text-[#ccff00]">Nostr Keys</span>
</h1>
<p className="text-gray-300 text-lg mb-6 max-w-2xl mx-auto">
Signer apps keep your private keys secure by signing events without exposing them.
Instead of pasting your key into every app, let a signer handle it safely.
</p>
<Alert className="bg-blue-950/50 border-blue-500/50 text-left max-w-xl mx-auto">
<Info className="h-4 w-4 text-blue-400" />
<AlertDescription className="text-blue-200 text-sm">
<strong>Why use a signer?</strong> Your Nostr private key is like a password that can never be changed.
A signer app keeps it safe while letting you use any Nostr app.
</AlertDescription>
</Alert>
</div>
</div>
</section>
{/* Signer Sections */}
<div className="container px-4 py-8 space-y-8">
{platformOrder.map((platform, index) => {
if (platform === 'android') {
return (
<PlatformSection
key="android"
icon={Smartphone}
title="Android Signers"
description="Native signer apps for Android devices, including GrapheneOS."
signers={androidSigners}
highlighted={isAndroid && index === 0}
/>
);
}
if (platform === 'ios') {
return (
<PlatformSection
key="ios"
icon={Apple}
title="iOS Signers"
description="Signer options for iPhone and iPad users."
signers={iosSigners}
highlighted={isIOS && index === 0}
/>
);
}
return (
<PlatformSection
key="browser"
icon={Chrome}
title="Browser Extensions"
description="Desktop browser extensions for signing on web apps."
signers={browserSigners}
highlighted={!isMobile && index === 0}
/>
);
})}
{/* Protocol Info */}
<section className="py-8 border-t border-gray-800">
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<Info className="h-5 w-5 text-gray-400" />
Understanding Protocols
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-black/50 border-gray-700/50">
<CardHeader className="pb-2">
<CardTitle className="text-base">NIP-07</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-400">
Browser extension standard. The extension injects signing capabilities into web apps.
Used by Alby, nos2x, Flamingo, and similar extensions.
</CardContent>
</Card>
<Card className="bg-black/50 border-gray-700/50">
<CardHeader className="pb-2">
<CardTitle className="text-base">NIP-46</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-400">
Remote signing protocol ("bunker"). Your signer app approves requests from other apps
without sharing your keys. Used by Nsec.app, Nostria, and Nowser.
</CardContent>
</Card>
<Card className="bg-black/50 border-gray-700/50">
<CardHeader className="pb-2">
<CardTitle className="text-base">NIP-55</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-400">
Android-specific signer protocol. Native apps can request signatures from Amber
without exposing keys. The most seamless Android experience.
</CardContent>
</Card>
<Card className="bg-black/50 border-gray-700/50">
<CardHeader className="pb-2">
<CardTitle className="text-base">Which to choose?</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-400">
<strong className="text-white">Android:</strong> Amber (NIP-55) for native apps<br />
<strong className="text-white">iOS:</strong> Nsec.app (NIP-46) for best compatibility<br />
<strong className="text-white">Desktop:</strong> Alby, nos2x, or Flamingo (NIP-07)
</CardContent>
</Card>
</div>
</section>
</div>
</div>
</>
);
}