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