diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 6e6dace..5991426 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -1,7 +1,4 @@ -// 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, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { Shield, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button.tsx'; import { Input } from '@/components/ui/input.tsx'; @@ -14,7 +11,8 @@ import { } from '@/components/ui/dialog.tsx'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.tsx'; import { useLoginActions } from '@/hooks/useLoginActions'; -import SignupDialog from './SignupDialog'; +import { useToast } from '@/hooks/useToast'; +import SignupFlow from './SignupFlow'; interface LoginDialogProps { isOpen: boolean; @@ -26,9 +24,19 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = const [isLoading, setIsLoading] = useState(false); const [nsec, setNsec] = useState(''); const [bunkerUri, setBunkerUri] = useState(''); - const [isSignupOpen, setIsSignupOpen] = useState(false); + const [view, setView] = useState<'login' | 'signup'>('login'); const fileInputRef = useRef(null); const login = useLoginActions(); + const { toast } = useToast(); + + useEffect(() => { + if (isOpen) { + setView('login'); + setIsLoading(false); + setNsec(''); + setBunkerUri(''); + } + }, [isOpen]); const handleExtensionLogin = () => { setIsLoading(true); @@ -41,6 +49,7 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = onClose(); } catch (error) { console.error('Extension login failed:', error); + toast({ title: 'Login Failed', description: (error as Error).message, variant: 'destructive' }); } finally { setIsLoading(false); } @@ -49,13 +58,13 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = const handleKeyLogin = () => { if (!nsec.trim()) return; setIsLoading(true); - try { login.nsec(nsec); onLogin(); onClose(); } catch (error) { console.error('Nsec login failed:', error); + toast({ title: 'Login Failed', description: 'Invalid nsec key.', variant: 'destructive' }); } finally { setIsLoading(false); } @@ -64,13 +73,13 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = const handleBunkerLogin = () => { if (!bunkerUri.trim() || !bunkerUri.startsWith('bunker://')) return; setIsLoading(true); - try { login.bunker(bunkerUri); onLogin(); onClose(); } catch (error) { console.error('Bunker login failed:', error); + toast({ title: 'Bunker Login Failed', description: 'Could not connect to bunker.', variant: 'destructive' }); } finally { setIsLoading(false); } @@ -79,7 +88,6 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; @@ -88,143 +96,103 @@ const LoginDialog: React.FC = ({ isOpen, onClose, onLogin }) = reader.readAsText(file); }; - const handleSignupClick = () => { - onClose(); - setIsSignupOpen(true); - }; - - return ( + const renderLoginView = () => ( <> - - - - Log in - - Access your account securely with your preferred method - - - -
- - - Extension - Nsec - Bunker - - - -
- -

- Login with one click using the browser extension -

- -
-
- - -
-
- - setNsec(e.target.value)} - className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' - placeholder='nsec1...' - /> -
- -
-

Or upload a key file

- - -
- - -
-
- - -
- - setBunkerUri(e.target.value)} - className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' - placeholder='bunker://' - /> - {bunkerUri && !bunkerUri.startsWith('bunker://') && ( -

URI must start with bunker://

- )} -
- - -
-
- -
- - -
- +
+
+ + + + Extension + Nsec + Bunker + + +
+ +

+ Login with one click using the browser extension +

+
-
- -
- setIsSignupOpen(false)} /> + + +
+
+ + setNsec(e.target.value)} className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' placeholder='nsec1...' /> +
+
+

Or upload a key file

+ + +
+ +
+
+ +
+ + setBunkerUri(e.target.value)} className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' placeholder='bunker://' /> + {bunkerUri && !bunkerUri.startsWith('bunker://') && (

URI must start with bunker://

)} +
+ +
+ + ); + + const renderSignupView = () => ( + <> + + Create Account + Create a new Nostr account. + +
+ setView('login')} onClose={onClose} /> +
+ + ); + + return ( + + + {view === 'login' ? renderLoginView() : renderSignupView()} + + + ); }; export default LoginDialog; diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx deleted file mode 100644 index 655f8f5..0000000 --- a/src/components/auth/SignupDialog.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Download, KeyRound, Lock, CheckCircle, Copy, ArrowRight } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Card, CardContent } from '@/components/ui/card'; -import { useToast } from '@/hooks/useToast'; -import { useLoginActions } from '@/hooks/useLoginActions'; -import { useNostrPublish } from '@/hooks/useNostrPublish'; -import { generateSecretKey, nip19 } from 'nostr-tools'; - -interface SignupDialogProps { - isOpen: boolean; - onClose: () => void; -} - -const SignupDialog: React.FC = ({ isOpen, onClose }) => { - const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome'); - const [isLoading, setIsLoading] = useState(false); - const [nsec, setNsec] = useState(''); - const [keySecured, setKeySecured] = useState<'none' | 'copied' | 'downloaded'>('none'); - const [profileData, setProfileData] = useState({ - name: '', - about: '', - }); - const { toast } = useToast(); - const login = useLoginActions(); - const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish(); - - const generateKey = () => { - setIsLoading(true); - setTimeout(() => { - try { - const sk = generateSecretKey(); - setNsec(nip19.nsecEncode(sk)); - setStep('download'); - toast({ - title: 'New key generated', - description: 'Keep it safe.', - }); - } catch { - toast({ - title: 'Error', - description: 'Failed to generate key. Please try again.', - variant: 'destructive', - }); - } finally { - setIsLoading(false); - } - }, 1000); - }; - - const downloadKey = () => { - try { - const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' }); - const url = globalThis.URL.createObjectURL(blob); - const filename = 'secret-key.txt'; - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.style.display = 'none'; - document.body.appendChild(a); - a.click(); - globalThis.URL.revokeObjectURL(url); - document.body.removeChild(a); - setKeySecured('downloaded'); - - toast({ - title: 'Key Secured', - description: 'Your key has been safely stored.', - }); - } catch { - toast({ - title: 'Download failed', - description: 'Could not download the key file. Please copy it manually.', - variant: 'destructive', - }); - } - }; - - const copyKey = () => { - navigator.clipboard.writeText(nsec); - setKeySecured('copied'); - toast({ - title: 'Copied to clipboard', - description: 'Your key has been copied.', - }); - }; - - const finishKeySetup = () => { - try { - login.nsec(nsec); - setStep('profile'); - } catch { - toast({ - title: 'Login Failed', - description: 'Failed to login with the generated key. Please try again.', - variant: 'destructive', - }); - } - }; - - const finishSignup = async (skipProfile = false) => { - try { - if (!skipProfile && (profileData.name || profileData.about)) { - const metadata: Record = {}; - if (profileData.name) metadata.name = profileData.name; - if (profileData.about) metadata.about = profileData.about; - - await publishEvent({ - kind: 0, - content: JSON.stringify(metadata), - }); - - toast({ - title: 'Profile Created', - description: 'Your profile has been created.', - }); - } - - setStep('done'); - setTimeout(() => { - onClose(); - toast({ - title: 'Welcome!', - description: 'You are now logged in.', - }); - }, 2000); - - } catch { - toast({ - title: 'Profile Setup Failed', - description: 'Your account was created but profile setup failed. You can update it later.', - variant: 'destructive', - }); - - setStep('done'); - setTimeout(() => { - onClose(); - toast({ - title: 'Welcome!', - description: 'You are now logged in.', - }); - }, 2000); - } - }; - - const getTitle = () => { - if (step === 'welcome') return 'Welcome'; - if (step === 'generate') return 'Generate Your Key'; - if (step === 'download') return 'Secure Your Key'; - if (step === 'profile') return 'Create Your Profile'; - return 'Welcome!'; - }; - - const getDescription = () => { - if (step === 'welcome') return 'Ready to get started?'; - if (step === 'generate') return 'A new key is being generated for you.'; - if (step === 'download') return "This key is your password. Keep it safe."; - if (step === 'profile') return 'This is how others will see you.'; - return 'You are now logged in.'; - }; - - useEffect(() => { - if (isOpen) { - setStep('welcome'); - setIsLoading(false); - setNsec(''); - setKeySecured('none'); - setProfileData({ name: '', about: '' }); - } - }, [isOpen]); - - return ( - - - - {getTitle()} - {getDescription()} - -
- {step === 'welcome' && ( -
-

- Your key is your identity. It's how you log in and how others see you. -

- -
- )} - - {step === 'generate' && ( -
- {isLoading ? ( -
-
- -
-

- Generating your key... -

-
- ) : ( -
- -

- Ready to generate your key? -

-
- )} - {!isLoading && ( - - )} -
- )} - - {step === 'download' && ( -
-
-
- - - Your Secret Key - -
-
- {nsec} -
-
- -
-

- Choose how to secure your key: -

- -
- - - - - - - - - - - -
- - -
-
- )} - - {step === 'profile' && ( -
-
-
- - setProfileData(prev => ({ ...prev, name: e.target.value }))} - placeholder='Your name' - disabled={isPublishing} - /> -
- -
- -