// NOTE: This file is stable and usually should not be modified. // It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. import React, { useState, useEffect } from 'react'; import { Shield, UserPlus, Sparkles, Cloud, Smartphone, Check, Loader2, KeyRound } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; interface LoginDialogProps { isOpen: boolean; onClose: () => void; onLogin: () => void; onSignup?: () => void; } export const LoginDialog: React.FC = ({ isOpen, onClose, onLogin, onSignup }) => { const [isLoading, setIsLoading] = useState(false); const [bunkerUri, setBunkerUri] = useState(''); const [bunkerError, setBunkerError] = useState(''); const [nsecInput, setNsecInput] = useState(''); const [nsecError, setNsecError] = useState(''); // Amber NIP-55 state const [amberStep, setAmberStep] = useState<'idle' | 'waiting' | 'paste' | 'done'>('idle'); const [pubkeyInput, setPubkeyInput] = useState(''); const [pubkeyError, setPubkeyError] = useState(''); const login = useLoginActions(); const { toast } = useToast(); const isAndroid = /Android/i.test(navigator.userAgent); // Step 1: open Amber asking it to copy pubkey to clipboard (no callback URL) const handleAmberLogin = () => { // nostrsigner: with no callbackUrl → Amber copies pubkey to clipboard // This works on ALL Android browsers including GrapheneOS Vanadium window.location.href = 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end'; setAmberStep('waiting'); }; // Step 2: user comes back — try clipboard automatically, else show paste input useEffect(() => { if (amberStep !== 'waiting') return; const handleVisible = async () => { if (document.visibilityState !== 'visible') return; // Try reading clipboard automatically try { const text = await navigator.clipboard.readText(); const clean = text.trim().toLowerCase(); if (/^[0-9a-f]{64}$/.test(clean)) { completeLogin(clean); return; } } catch { /* clipboard permission denied — fall through to paste UI */ } // Show paste input setAmberStep('paste'); }; document.addEventListener('visibilitychange', handleVisible); return () => document.removeEventListener('visibilitychange', handleVisible); }, [amberStep]); const completeLogin = (pubkey: string) => { try { login.amber(pubkey); setAmberStep('done'); toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); setTimeout(() => { onLogin(); onClose(); }, 800); } catch (e) { console.error('[Amber] login.amber() failed:', e); setAmberStep('idle'); toast({ title: 'Login failed', description: 'Could not log in with Amber. Try again.', variant: 'destructive' }); } }; const handlePubkeySubmit = () => { const clean = pubkeyInput.trim().toLowerCase(); if (!/^[0-9a-f]{64}$/.test(clean)) { setPubkeyError('Must be a 64-character hex pubkey'); return; } completeLogin(clean); }; // Reset when dialog closes useEffect(() => { if (!isOpen) { setAmberStep('idle'); setPubkeyInput(''); setPubkeyError(''); setBunkerUri(''); setBunkerError(''); setNsecInput(''); setNsecError(''); setIsLoading(false); } }, [isOpen]); const handleBunkerLogin = async () => { const uri = bunkerUri.trim(); if (!uri.startsWith('bunker://')) { setBunkerError('Must start with bunker://'); return; } setIsLoading(true); setBunkerError(''); try { await login.bunker(uri); toast({ title: '🛹 Logged in!', description: 'Connected via bunker!' }); onLogin(); onClose(); } catch (e: unknown) { setBunkerError(`Failed: ${(e as Error)?.message ?? 'Check your bunker URI'}`); } finally { setIsLoading(false); } }; const handleNsecLogin = () => { const val = nsecInput.trim(); if (!val.startsWith('nsec1')) { setNsecError('Must start with nsec1'); return; } try { login.nsec(val); toast({ title: '🛹 Logged in!' }); onLogin(); onClose(); } catch (e: unknown) { setNsecError(`Invalid nsec: ${(e as Error)?.message ?? 'Check your key'}`); } }; const handleExtensionLogin = async () => { try { await login.extension(); onLogin(); onClose(); } catch { toast({ title: 'No extension found', description: 'Install Alby or another NIP-07 extension.', variant: 'destructive' }); } }; return ( Sign up or log in to continue
{/* Sign Up */}
New here?

Create a free account. No email required.

Or log in
Amber Bunker nsec Ext {/* ── AMBER TAB ── */} {/* Done */} {amberStep === 'done' && (

Logged in!

)} {/* Step 1 idle — show login button */} {amberStep === 'idle' && (

Login with Amber

Opens Amber → approve → come back here.

)} {/* Step 2 waiting — app just navigated to Amber */} {amberStep === 'waiting' && (

Approve in Amber

Tap "Get public key" in Amber, then come back here.

)} {/* Step 3 paste — clipboard read failed or user chose manual */} {amberStep === 'paste' && (

Paste your pubkey

In Amber: tap your profile → copy public key (hex). Paste it below.

{ setPubkeyInput(e.target.value); setPubkeyError(''); }} className='bg-black/60 border-[#ccff00]/30 text-white placeholder:text-gray-600 text-xs font-mono' autoComplete='off' autoCorrect='off' spellCheck={false} /> {pubkeyError &&

{pubkeyError}

}
)}
{/* ── BUNKER TAB ── */}

Bunker (NIP-46)

Paste your bunker:// URI.

Amber: Settings → NIP-46 Bunker → Create → Copy

nsec.app: Connect app → Copy bunker URI

{ setBunkerUri(e.target.value); setBunkerError(''); }} className='bg-black/60 border-blue-500/30 text-white placeholder:text-gray-600 text-xs font-mono' autoComplete='off' autoCorrect='off' spellCheck={false} /> {bunkerError &&

{bunkerError}

}
{/* ── NSEC TAB ── */}

Secret Key (nsec)

Paste your Nostr secret key directly.

⚠️ Only use this on a trusted device. Never share your nsec.

{ setNsecInput(e.target.value); setNsecError(''); }} className='bg-black/60 border-yellow-500/30 text-white placeholder:text-gray-600 text-xs font-mono' autoComplete='off' autoCorrect='off' spellCheck={false} /> {nsecError &&

{nsecError}

}
{/* ── EXTENSION TAB ── */}

Browser Extension (NIP-07)

Best for desktop. Works with Alby, nos2x, Flamingo.

); }; export default LoginDialog;