- 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.
364 lines
14 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|