diff --git a/src/components/auth/LoginArea.tsx b/src/components/auth/LoginArea.tsx index f84414a..4663d99 100644 --- a/src/components/auth/LoginArea.tsx +++ b/src/components/auth/LoginArea.tsx @@ -57,6 +57,7 @@ export function LoginArea({ className }: LoginAreaProps) { setSignupDialogOpen(false)} + onLogin={handleLogin} /> ); diff --git a/src/components/auth/NostrExtensionIndicator.tsx b/src/components/auth/NostrExtensionIndicator.tsx new file mode 100644 index 0000000..b9680fd --- /dev/null +++ b/src/components/auth/NostrExtensionIndicator.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useLoginActions } from '@/hooks/useLoginActions'; + +interface NostrExtensionIndicatorProps { + onLogin?: () => void; + onClose?: () => void; +} + +const NostrExtensionIndicator: React.FC = ({ onLogin, onClose }) => { + const login = useLoginActions(); + + const handleExtensionLogin = async () => { + try { + await login.extension(); + onLogin?.(); + onClose?.(); + } catch (error) { + console.error('Extension login failed:', error); + } + }; + + function renderBody(): React.ReactNode { + if ('nostr' in window) { + return ( + <> + + {' '}with browser extension. + + ); + } else { + return 'Browser extension not found.'; + } + } + + return ( +
+

+ {renderBody()} +

+
+ ); +}; + +export default NostrExtensionIndicator; \ No newline at end of file diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index b80f333..daf5552 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -1,296 +1,54 @@ // 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, useRef } from 'react'; -import { Download, Key, UserPlus, FileText, Shield, User, Sparkles, LogIn, CheckCircle, Upload, Globe } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; + import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Card, CardContent } from '@/components/ui/card'; + import { toast } from '@/hooks/useToast'; -import { useLoginActions } from '@/hooks/useLoginActions'; -import { useNostrPublish } from '@/hooks/useNostrPublish'; -import { useUploadFile } from '@/hooks/useUploadFile'; -import { generateSecretKey, nip19 } from 'nostr-tools'; + +import NostrExtensionIndicator from './NostrExtensionIndicator'; import { cn } from '@/lib/utils'; interface SignupDialogProps { isOpen: boolean; onClose: () => void; onComplete?: () => void; + onLogin?: () => void; } -const sanitizeFilename = (filename: string) => { - return filename.replace(/[^a-z0-9_.-]/gi, '_'); -} +const SignupDialog: React.FC = ({ isOpen, onClose, onLogin }) => { + const [step, setStep] = useState<'key' | 'keygen'>('key'); -const SignupDialog: React.FC = ({ isOpen, onClose, onComplete }) => { - const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome'); - const [isLoading, setIsLoading] = useState(false); - const [nsec, setNsec] = useState(''); - const [showSparkles, setShowSparkles] = useState(false); - const [keySecured, setKeySecured] = useState<'none' | 'downloaded'>('none'); - const [profileData, setProfileData] = useState({ - name: '', - about: '', - picture: '' - }); - const login = useLoginActions(); - const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish(); - const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); - const avatarFileInputRef = useRef(null); - - // Generate a proper nsec key using nostr-tools - const generateKey = () => { - setIsLoading(true); - setShowSparkles(true); - - // Add a dramatic pause for the key generation effect - setTimeout(() => { - try { - // Generate a new secret key - const sk = generateSecretKey(); - - // Convert to nsec format - setNsec(nip19.nsecEncode(sk)); - setStep('download'); - - toast({ - title: 'Your Secret Key is Ready!', - description: 'A new secret key has been generated for you.', - }); - } catch { - toast({ - title: 'Error', - description: 'Failed to generate key. Please try again.', - variant: 'destructive', - }); - } finally { - setIsLoading(false); - setShowSparkles(false); - } - }, 2000); + const handleGenerateKey = () => { + setStep('keygen'); }; - const downloadKey = () => { - try { - // Create a blob with the key text - const blob = new Blob([nsec], { type: 'text/plain; charset=utf-8' }); - const url = globalThis.URL.createObjectURL(blob); - - // Sanitize filename - const filename = sanitizeFilename('nostr-nsec-key.txt'); - - // Create a temporary link element and trigger download - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.style.display = 'none'; - document.body.appendChild(a); - a.click(); - - // Clean up immediately - globalThis.URL.revokeObjectURL(url); - document.body.removeChild(a); - - // Mark as secured - setKeySecured('downloaded'); - - toast({ - title: 'Secret Key Saved!', - 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 handleHasKey = () => { + onClose(); + // This would trigger the login dialog to open with the key-add step + // For now, we'll just show a toast message + toast({ + title: 'Use Login Instead', + description: 'Please use the login dialog to enter your existing key.', + }); }; - - - 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 handleExtensionLogin = () => { + if (onLogin) { + onLogin(); } - }; - - const handleAvatarUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - - // Reset file input - e.target.value = ''; - - // Validate file type - if (!file.type.startsWith('image/')) { - toast({ - title: 'Invalid file type', - description: 'Please select an image file for your avatar.', - variant: 'destructive', - }); - return; - } - - // Validate file size (max 5MB) - if (file.size > 5 * 1024 * 1024) { - toast({ - title: 'File too large', - description: 'Avatar image must be smaller than 5MB.', - variant: 'destructive', - }); - return; - } - - try { - const tags = await uploadFile(file); - // Get the URL from the first tag - const url = tags[0]?.[1]; - if (url) { - setProfileData(prev => ({ ...prev, picture: url })); - toast({ - title: 'Avatar uploaded!', - description: 'Your avatar has been uploaded successfully.', - }); - } - } catch { - toast({ - title: 'Upload failed', - description: 'Failed to upload avatar. Please try again.', - variant: 'destructive', - }); - } - }; - - const finishSignup = async (skipProfile = false) => { - // Mark signup completion time for fallback welcome modal - localStorage.setItem('signup_completed', Date.now().toString()); - - try { - // Publish profile if user provided information - if (!skipProfile && (profileData.name || profileData.about || profileData.picture)) { - const metadata: Record = {}; - if (profileData.name) metadata.name = profileData.name; - if (profileData.about) metadata.about = profileData.about; - if (profileData.picture) metadata.picture = profileData.picture; - - await publishEvent({ - kind: 0, - content: JSON.stringify(metadata), - }); - - toast({ - title: 'Profile Created!', - description: 'Your profile has been set up.', - }); - } - - // Close signup and show welcome modal - onClose(); - if (onComplete) { - // Add a longer delay to ensure login state has fully propagated - setTimeout(() => { - onComplete(); - }, 600); - } else { - // Fallback for when used without onComplete - setStep('done'); - setTimeout(() => { - onClose(); - toast({ - title: 'Welcome!', - description: 'Your account is ready.', - }); - }, 3000); - } - } catch { - toast({ - title: 'Profile Setup Failed', - description: 'Your account was created but profile setup failed. You can update it later.', - variant: 'destructive', - }); - - // Still proceed to completion even if profile failed - onClose(); - if (onComplete) { - // Add a longer delay to ensure login state has fully propagated - setTimeout(() => { - onComplete(); - }, 600); - } else { - // Fallback for when used without onComplete - setStep('done'); - setTimeout(() => { - onClose(); - toast({ - title: 'Welcome!', - description: 'Your account is ready.', - }); - }, 3000); - } - } - }; - - const getTitle = () => { - if (step === 'welcome') return ( - - Create Your Account - - ); - if (step === 'generate') return ( - - Generating Your Key - - ); - if (step === 'download') return ( - - Secret Key - - ); - if (step === 'profile') return ( - - Create Your Profile - - ); - return ( - - Welcome! - - ); + onClose(); }; // Reset state when dialog opens useEffect(() => { if (isOpen) { - setStep('welcome'); - setIsLoading(false); - setNsec(''); - setShowSparkles(false); - setKeySecured('none'); - setProfileData({ name: '', about: '', picture: '' }); + setStep('key'); } }, [isOpen]); - // Add sparkle animation effect - useEffect(() => { - if (showSparkles) { - const interval = setInterval(() => { - // This will trigger re-renders for sparkle animation - }, 100); - return () => clearInterval(interval); - } - }, [showSparkles]); - return ( = ({ isOpen, onClose, onComplete > - {getTitle()} + Sign up -
- {/* Welcome Step - New engaging introduction */} - {step === 'welcome' && ( -
- {/* Hero illustration */} -
-
-
- - -
- -
- - -
-
- {/* Benefits */} -
-
- - Decentralized and censorship-resistant -
-
- - You are in control of your data -
-
- - Join a global network -
-
+
+ {step === 'key' && ( +
+

+ You need a key to continue +

+ +
+ 🔑
-
+
+ +
)} - {/* Generate Step - Enhanced with animations */} - {step === 'generate' && ( + {step === 'keygen' && (
-
- {/* Animated background elements */} - {showSparkles && ( -
- {[...Array(12)].map((_, i) => ( - - ))} -
- )} - -
- {isLoading ? ( -
-
- -
-
-
-
-
-

- - Generating your secret key... -

-

- Creating your secure key -

-
-
- ) : ( -
- -
-

- Ready to generate your secret key? -

-

- This key will be your password to access applications within the Nostr network. -

- -
-
- )} -
+
+

+ Key Generation Coming Soon +

+

+ Key generation feature is being developed. For now, please use an existing key or browser extension. +

- {!isLoading && ( - - )} +
)} - {/* Download Step - Whimsical and magical */} - {step === 'download' && ( -
- {/* Key reveal */} -
- {/* Sparkles */} -
- - - - -
- -
-
-
- -
-
- -
-
-
- -
-

- Your secret key has been generated! -

- - {/* Warning */} -
-
-
- - - Important Warning - -
-

- This key is your primary and only means of accessing your account. Store it safely and securely. -

-
-
-
+
+
+
+
- - {/* Key vault */} - - - {/* Security options */} -
- - -
- {/* Download Option */} - - - - - - - -
- - {/* Continue button */} - +
+ + or +
- )} - {/* Profile Step - Optional profile setup */} - {step === 'profile' && ( -
- {/* Profile setup illustration */} -
- {/* Sparkles */} -
- - - -
- -
-
-
- -
-
- -
-
-
- -
-

- Almost there! Let's set up your profile -

- -

- Your profile is your identity on Nostr. -

-
-
- - {/* Publishing status indicator */} - {isPublishing && ( -
-
-
- - Publishing your profile... - -
-
- )} - - {/* Profile form */} -
-
- - setProfileData(prev => ({ ...prev, name: e.target.value }))} - placeholder='Your name' - className='rounded-lg' - disabled={isPublishing} - /> -
- -
- -