// 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, Lock, CheckCircle, Copy, Upload, Globe, FileSignature, Wand2 } 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 { 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 { cn } from '@/lib/utils'; interface SignupDialogProps { isOpen: boolean; onClose: () => void; onComplete?: () => void; } const sanitizeFilename = (filename: string) => { return filename.replace(/[^a-z0-9_.-]/gi, '_'); } 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' | 'copied' | '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 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('secret-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. Keep it safe!', }); } 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: 'Key copied to clipboard.', }); }; 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 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 ( Secure Your Secret Key ); if (step === 'profile') return ( Create Your Profile ); return ( Welcome! ); }; const getDescription = () => { if (step === 'welcome') return 'Ready to join the Nostr network?'; if (step === 'generate') return 'Creating your secret key to access Nostr.'; if (step === 'download') return 'This key is your password - keep it safe!'; if (step === 'profile') return 'Tell others about yourself.'; return 'Your account is ready!'; }; // Reset state when dialog opens useEffect(() => { if (isOpen) { setStep('welcome'); setIsLoading(false); setNsec(''); setShowSparkles(false); setKeySecured('none'); setProfileData({ name: '', about: '', picture: '' }); } }, [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 ( {getTitle()} {getDescription()}
{/* 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

Join the Nostr network and take control of your social media experience. Your journey begins by generating a secret key.

Free forever • Decentralized • Your data, your control

)} {/* Generate Step - Enhanced with animations */} {step === 'generate' && (
{/* 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.

It's completely unique and secure:

keep it secret, keep it safe!

)}
{!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 */}
Your Secret Key
{nsec}
{/* Security options */}

Choose how to secure your key:

{/* Copy Option */} {/* Download Option */}
{/* Continue button */}
)} {/* 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} />