SkateSpots/src/pages/SignersPage.tsx

379 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';
import { useTranslation } from '@/contexts/language';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrLogin } from '@nostrify/react/login';
import { isInteractiveSigner } from '@/lib/signerCapabilities';
// 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: 'settingsUi.signers.amberDesc',
features: ['settingsUi.signers.fNativeApp', 'settingsUi.signers.fMultiKey', 'settingsUi.signers.fEventSigning', 'settingsUi.signers.fSecureStorage'],
url: 'https://github.com/greenart7c3/Amber',
recommended: true,
platforms: ['android'],
protocol: 'NIP-55',
},
{
name: 'Nsec.app',
description: 'settingsUi.signers.nsecAppDesc',
features: ['settingsUi.signers.fRemoteSigning', 'settingsUi.signers.fAnyClient', 'settingsUi.signers.fCrossDevice'],
url: 'https://nsec.app',
platforms: ['android', 'ios', 'web'],
protocol: 'NIP-46',
},
{
name: 'Nostria Signer',
description: 'settingsUi.signers.nostriaDesc',
features: ['settingsUi.signers.fSecureStorage', 'settingsUi.signers.fRemoteSigning', 'settingsUi.signers.fMultiKeySupport'],
url: 'https://nostria.app',
platforms: ['android', 'ios'],
protocol: 'NIP-46',
},
{
name: 'Nowser',
description: 'settingsUi.signers.nowserDesc',
features: ['settingsUi.signers.fNip07', 'settingsUi.signers.fNip46', 'settingsUi.signers.fFlexibleSigning'],
url: 'https://nowser.app',
platforms: ['android', 'ios'],
protocol: 'NIP-07 / NIP-46',
},
];
const iosSigners: SignerApp[] = [
{
name: 'Nsec.app',
description: 'settingsUi.signers.nsecAppIosDesc',
features: ['settingsUi.signers.fRemoteSigning', 'settingsUi.signers.fAnyClient', 'settingsUi.signers.fCrossDevice'],
url: 'https://nsec.app',
recommended: true,
platforms: ['android', 'ios', 'web'],
protocol: 'NIP-46',
},
{
name: 'Nowser (TestFlight)',
description: 'settingsUi.signers.nowserTfDesc',
features: ['settingsUi.signers.fNip07', 'settingsUi.signers.fNip46', 'settingsUi.signers.fNativeIos'],
url: 'https://testflight.apple.com/join/nowser',
platforms: ['ios'],
protocol: 'NIP-07 / NIP-46',
},
{
name: 'Nostria Signer',
description: 'settingsUi.signers.nostriaIosDesc',
features: ['settingsUi.signers.fSecureStorage', 'settingsUi.signers.fRemoteSigning', 'settingsUi.signers.fMultiKeySupport'],
url: 'https://nostria.app',
platforms: ['android', 'ios'],
protocol: 'NIP-46',
},
];
const browserSigners: SignerApp[] = [
{
name: 'nos2x',
description: 'settingsUi.signers.nos2xDesc',
features: ['settingsUi.signers.fLightweight', 'settingsUi.signers.fEasyToUse', 'settingsUi.signers.fChromeSupport'],
url: 'https://chrome.google.com/webstore/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp',
recommended: true,
platforms: ['web'],
protocol: 'NIP-07',
},
{
name: 'Alby',
description: 'settingsUi.signers.albyDesc',
features: ['settingsUi.signers.fLightningPayments', 'settingsUi.signers.fNostrSigning', 'settingsUi.signers.fWebln', 'settingsUi.signers.fNwc'],
url: 'https://getalby.com',
platforms: ['web', 'desktop'],
protocol: 'NIP-07',
},
{
name: 'Flamingo',
description: 'settingsUi.signers.flamingoDesc',
features: ['settingsUi.signers.fNostrSigning', 'settingsUi.signers.fCleanUi', 'settingsUi.signers.fPrivacyFocused'],
url: 'https://www.flamingowallet.com',
platforms: ['web'],
protocol: 'NIP-07',
},
{
name: 'Kiwi Browser + Extension',
description: 'settingsUi.signers.kiwiDesc',
features: ['settingsUi.signers.fExtensionMobile', 'settingsUi.signers.fFullBrowser', 'settingsUi.signers.fGraphene'],
url: 'https://kiwibrowser.com',
platforms: ['android'],
protocol: 'NIP-07',
},
];
function SignerCard({ signer }: { signer: SignerApp }) {
const { t } = useTranslation();
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" />
{t('settingsUi.signers.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">
{t(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" />
{t(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" />
{t('settingsUi.signers.get', { name: 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;
}) {
const { t } = useTranslation();
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">
{t('settingsUi.signers.yourPlatform')}
</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() {
const { t } = useTranslation();
const { user } = useCurrentUser();
const { logins } = useNostrLogin();
const amber = isInteractiveSigner(user, logins);
useSeoMeta({
title: t('settingsUi.signers.seoTitle'),
description: t('settingsUi.signers.seoDescription'),
});
// 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">{t('settingsUi.signers.securityGuide')}</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">{t('settingsUi.signers.protect')}</span>{' '}
<span className="text-[#ccff00]">{t('settingsUi.signers.nostrKeys')}</span>
</h1>
<p className="text-gray-300 text-lg mb-6 max-w-2xl mx-auto">
{t('settingsUi.signers.heroDesc')}
</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>{t('settingsUi.signers.whySignerStrong')}</strong> {t('settingsUi.signers.whySigner')}
</AlertDescription>
</Alert>
{amber && (
<Alert className="mt-4 bg-amber-950/40 border-amber-500/50 text-left max-w-xl mx-auto">
<Shield className="h-4 w-4 text-amber-300" />
<AlertDescription className="text-amber-100 text-sm">
<strong>{t('signerPromptUi.title')}</strong>
<span className="mt-1 block text-amber-100/80">{t('signerPromptUi.body')}</span>
<span className="mt-1 block text-amber-200/80">{t('signerPromptUi.bunker')}</span>
</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={t('settingsUi.signers.androidTitle')}
description={t('settingsUi.signers.androidDesc')}
signers={androidSigners}
highlighted={isAndroid && index === 0}
/>
);
}
if (platform === 'ios') {
return (
<PlatformSection
key="ios"
icon={Apple}
title={t('settingsUi.signers.iosTitle')}
description={t('settingsUi.signers.iosDesc')}
signers={iosSigners}
highlighted={isIOS && index === 0}
/>
);
}
return (
<PlatformSection
key="browser"
icon={Chrome}
title={t('settingsUi.signers.browserTitle')}
description={t('settingsUi.signers.browserDesc')}
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" />
{t('settingsUi.signers.protocolsTitle')}
</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">
{t('settingsUi.signers.nip07Desc')}
</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">
{t('settingsUi.signers.nip46Desc')}
</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">
{t('settingsUi.signers.nip55Desc')}
</CardContent>
</Card>
<Card className="bg-black/50 border-gray-700/50">
<CardHeader className="pb-2">
<CardTitle className="text-base">{t('settingsUi.signers.whichChoose')}</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-400">
<strong className="text-white">{t('settingsUi.signers.chooseAndroid')}</strong> {t('settingsUi.signers.chooseAndroidDesc')}<br />
<strong className="text-white">{t('settingsUi.signers.chooseIos')}</strong> {t('settingsUi.signers.chooseIosDesc')}<br />
<strong className="text-white">{t('settingsUi.signers.chooseDesktop')}</strong> {t('settingsUi.signers.chooseDesktopDesc')}
</CardContent>
</Card>
</div>
</section>
</div>
</div>
</>
);
}