UX Move 1: kill the signup wall — one-tap silent account, no forced key download
- "Sign Up" now goes straight into a one-tap flow: a key is generated silently and the user is logged in immediately, landing on optional profile setup. - Removed the method-choice dialog (mnemonic vs nsec) and the generate/download steps from the default path — no crypto vocabulary, no gates. - Removed the forced key-download gate (Continue was disabled until download). Backup is now optional and deferred (BackupReminder already nudges post-login; the "done" step offers a gentle, jargon-free recovery-code reveal). - Softened copy throughout: "secret key" -> "recovery code / account backup", "your identity on Nostr" -> plain reassurance, CTA -> "Save & Start Skating". - Advanced signer-app path preserved under a subtle link. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
33cfe692ed
commit
54e6b4a809
2 changed files with 97 additions and 453 deletions
|
|
@ -40,7 +40,7 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
|
|||
// Compact mode for mobile headers - icon only
|
||||
<div className="flex gap-1 justify-center">
|
||||
<Button
|
||||
onClick={() => setSignupMethodDialogOpen(true)}
|
||||
onClick={() => setNsecSignupDialogOpen(true)}
|
||||
size="icon"
|
||||
className='h-9 w-9 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
|
||||
title="Sign Up"
|
||||
|
|
@ -61,7 +61,7 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
|
|||
// Default full-size buttons
|
||||
<div className="flex gap-3 justify-center w-full">
|
||||
<Button
|
||||
onClick={() => setSignupMethodDialogOpen(true)}
|
||||
onClick={() => setNsecSignupDialogOpen(true)}
|
||||
className='flex items-center gap-2 px-5 py-2.5 rounded-full bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold transition-all flex-1 sm:flex-none'
|
||||
>
|
||||
<UserPlus className='w-4 h-4' />
|
||||
|
|
@ -82,7 +82,7 @@ export function LoginArea({ className, compact }: LoginAreaProps) {
|
|||
isOpen={loginDialogOpen}
|
||||
onClose={() => setLoginDialogOpen(false)}
|
||||
onLogin={handleLogin}
|
||||
onSignup={() => setSignupMethodDialogOpen(true)}
|
||||
onSignup={() => setNsecSignupDialogOpen(true)}
|
||||
/>
|
||||
|
||||
<SignupMethodDialog
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Download, Key, UserPlus, FileText, Shield, User, Sparkles, LogIn, CheckCircle, Upload, Globe, Eye, EyeOff, ExternalLink } from 'lucide-react';
|
||||
import { Download, Key, Shield, User, Sparkles, LogIn, CheckCircle, Upload } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
|
@ -13,7 +13,7 @@ import { toast } from '@/hooks/useToast';
|
|||
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||
import { useUploadFile } from '@/hooks/useUploadFile';
|
||||
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { generateSecretKey, nip19 } from 'nostr-tools';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SignupDialogProps {
|
||||
|
|
@ -27,13 +27,10 @@ const sanitizeFilename = (filename: string) => {
|
|||
}
|
||||
|
||||
const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete }) => {
|
||||
const [step, setStep] = useState<'welcome' | 'generate' | 'download' | 'profile' | 'done'>('welcome');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [step, setStep] = useState<'profile' | 'done'>('profile');
|
||||
const [nsec, setNsec] = useState('');
|
||||
const [showSparkles, setShowSparkles] = useState(false);
|
||||
const [keySecured, setKeySecured] = useState<'none' | 'downloaded'>('none');
|
||||
const [showNsec, setShowNsec] = useState(false);
|
||||
const [showNpub, setShowNpub] = useState(false);
|
||||
const [profileData, setProfileData] = useState({
|
||||
name: '',
|
||||
about: '',
|
||||
|
|
@ -44,36 +41,24 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const avatarFileInputRef = useRef<HTMLInputElement>(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);
|
||||
// Silently generate a key and log the user in immediately.
|
||||
// No forced download, no crypto vocabulary — the account "just works".
|
||||
// Backup is deferred to the BackupReminder (shown a couple seconds after login).
|
||||
const createAccountSilently = () => {
|
||||
try {
|
||||
const sk = generateSecretKey();
|
||||
const newNsec = nip19.nsecEncode(sk);
|
||||
setNsec(newNsec);
|
||||
login.nsec(newNsec);
|
||||
return true;
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description: "Couldn't create your account. Please try again.",
|
||||
variant: 'destructive',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadKey = () => {
|
||||
|
|
@ -115,19 +100,6 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
|
||||
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
|
@ -227,24 +199,9 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (step === 'welcome') return (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
Create Your Account
|
||||
</span>
|
||||
);
|
||||
if (step === 'generate') return (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
Generating Your Key
|
||||
</span>
|
||||
);
|
||||
if (step === 'download') return (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
Secret Key
|
||||
</span>
|
||||
);
|
||||
if (step === 'profile') return (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
Create Your Profile
|
||||
Set Up Your Profile
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
|
|
@ -254,30 +211,20 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
);
|
||||
};
|
||||
|
||||
// Reset state when dialog opens
|
||||
// Reset state when dialog opens, and silently create + log in the account
|
||||
// so the skater lands straight on profile setup — no crypto steps, no gates.
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setStep('welcome');
|
||||
setIsLoading(false);
|
||||
setStep('profile');
|
||||
setNsec('');
|
||||
setShowSparkles(false);
|
||||
setKeySecured('none');
|
||||
setShowNsec(false);
|
||||
setShowNpub(false);
|
||||
setProfileData({ name: '', about: '', picture: '' });
|
||||
createAccountSilently();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
|
|
@ -289,350 +236,6 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='px-4 sm:px-6 pt-2 pb-4 space-y-4 overflow-y-auto flex-1'>
|
||||
{/* Welcome Step - New engaging introduction */}
|
||||
{step === 'welcome' && (
|
||||
<div className='text-center space-y-4'>
|
||||
{/* Hero illustration */}
|
||||
<div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50'>
|
||||
<div className='flex justify-center items-center space-x-4 mb-3'>
|
||||
<div className='relative'>
|
||||
<UserPlus className='w-12 h-12 text-blue-600' />
|
||||
<Sparkles className='w-4 h-4 text-yellow-500 absolute -top-1 -right-1 animate-pulse' />
|
||||
</div>
|
||||
<Globe className='w-16 h-16 text-blue-700 animate-spin-slow' />
|
||||
<div className='relative'>
|
||||
<FileText className='w-12 h-12 text-blue-600' />
|
||||
<Sparkles className='w-4 h-4 text-yellow-500 absolute -top-1 -left-1 animate-pulse' style={{animationDelay: '0.3s'}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Benefits */}
|
||||
<div className='grid grid-cols-1 gap-2 text-sm'>
|
||||
<div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'>
|
||||
<Shield className='w-4 h-4' />
|
||||
Community-run, skater-owned
|
||||
</div>
|
||||
<div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'>
|
||||
<User className='w-4 h-4' />
|
||||
You control your account
|
||||
</div>
|
||||
<div className='flex items-center justify-center gap-2 text-blue-700 dark:text-blue-300'>
|
||||
<Globe className='w-4 h-4' />
|
||||
Connect with skaters worldwide
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<Button
|
||||
className='w-full rounded-full py-6 text-lg font-bold uppercase bg-[#ccff00] text-black hover:bg-[#ccff00]/90 transform transition-all duration-200 hover:scale-105 shadow-[0_0_20px_rgba(204,255,0,0.5)]'
|
||||
onClick={() => setStep('generate')}
|
||||
>
|
||||
<LogIn className='w-5 h-5 mr-2' />
|
||||
Get Started
|
||||
</Button>
|
||||
<a href="/onboarding" onClick={onClose}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='w-full rounded-full py-3 border-2 border-[#ff0040]/50 hover:bg-[#ff0040]/10 text-[#ff0040] font-bold uppercase'
|
||||
>
|
||||
<Sparkles className='w-4 h-4 mr-2' />
|
||||
Need Help? View Guide
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate Step - Enhanced with animations */}
|
||||
{step === 'generate' && (
|
||||
<div className='text-center space-y-4'>
|
||||
<div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-purple-100 dark:from-blue-950/50 dark:to-purple-950/50 overflow-hidden'>
|
||||
{/* Animated background elements */}
|
||||
{showSparkles && (
|
||||
<div className='absolute inset-0'>
|
||||
{[...Array(12)].map((_, i) => (
|
||||
<Sparkles
|
||||
key={i}
|
||||
className={`absolute w-4 h-4 text-yellow-400 animate-ping`}
|
||||
style={{
|
||||
left: `${Math.random() * 80 + 10}%`,
|
||||
top: `${Math.random() * 80 + 10}%`,
|
||||
animationDelay: `${Math.random() * 2}s`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='relative z-10'>
|
||||
{isLoading ? (
|
||||
<div className='space-y-3'>
|
||||
<div className='relative'>
|
||||
<Key className='w-20 h-20 text-primary mx-auto animate-pulse' />
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<div className='w-24 h-24 border-4 border-yellow-400 border-t-transparent rounded-full animate-spin'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<p className='text-lg font-semibold text-primary flex items-center justify-center gap-2'>
|
||||
<Sparkles className='w-5 h-5' />
|
||||
Generating your secret key...
|
||||
</p>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Creating your secure key
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-3'>
|
||||
<Key className='w-20 h-20 text-primary mx-auto' />
|
||||
<div className='space-y-2'>
|
||||
<p className='text-lg font-semibold'>
|
||||
Ready to generate your secret key?
|
||||
</p>
|
||||
<p className='text-sm text-muted-foreground px-5'>
|
||||
This key will be your password to access applications within the Nostr network.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLoading && (
|
||||
<Button
|
||||
className='w-full rounded-full py-6 text-lg font-semibold bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 transform transition-all duration-200 hover:scale-105 shadow-lg'
|
||||
onClick={generateKey}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Sparkles className='w-5 h-5 mr-2' />
|
||||
Generate My Secret Key
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Step - Whimsical and magical */}
|
||||
{step === 'download' && (
|
||||
<div className='text-center space-y-4'>
|
||||
{/* Key reveal */}
|
||||
<div className='relative p-6 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-950/50 dark:to-indigo-950/50 overflow-hidden'>
|
||||
{/* Sparkles */}
|
||||
<div className='absolute inset-0 pointer-events-none'>
|
||||
<Sparkles className='absolute top-3 left-4 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '0s'}} />
|
||||
<Sparkles className='absolute top-6 right-6 w-3 h-3 text-yellow-500 animate-pulse' style={{animationDelay: '0.5s'}} />
|
||||
<Sparkles className='absolute bottom-4 left-6 w-3 h-3 text-yellow-400 animate-pulse' style={{animationDelay: '1s'}} />
|
||||
<Sparkles className='absolute bottom-3 right-4 w-3 h-3 text-yellow-500 animate-pulse' style={{animationDelay: '1.5s'}} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 flex justify-center items-center mb-3'>
|
||||
<div className='relative'>
|
||||
<div className='w-16 h-16 bg-gradient-to-br from-blue-200 to-indigo-300 rounded-full flex items-center justify-center shadow-lg animate-pulse'>
|
||||
<Key className='w-8 h-8 text-indigo-800' />
|
||||
</div>
|
||||
<div className='absolute -top-1 -right-1 w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center animate-bounce'>
|
||||
<Sparkles className='w-3 h-3 text-white' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 space-y-2'>
|
||||
<p className='text-base font-semibold'>
|
||||
Your secret key has been generated!
|
||||
</p>
|
||||
|
||||
{/* Warning */}
|
||||
<div className='relative mx-auto max-w-sm'>
|
||||
<div className='p-3 bg-gradient-to-r from-amber-100 via-yellow-50 to-amber-100 dark:from-amber-950/40 dark:via-yellow-950/20 dark:to-amber-950/40 rounded-lg border-2 border-amber-300 dark:border-amber-700 shadow-md'>
|
||||
<div className='flex items-center gap-2 mb-1'>
|
||||
<FileText className='w-3 h-3 text-amber-700' />
|
||||
<span className='text-xs font-bold text-amber-800 dark:text-amber-200'>
|
||||
Important Warning
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-xs text-red-700 dark:text-amber-300 italic'>
|
||||
This key is your primary and only means of accessing your account. Store it safely and securely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Your Keys - nsec and npub with copy buttons and privacy controls */}
|
||||
<Card className="bg-black/50 border-[#ff0040]/30">
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* nsec (secret key) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-bold uppercase text-[#ff0040]">🔐 Secret Key (nsec)</label>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 px-2 text-xs hover:bg-[#ff0040]/10 text-[#ff0040] font-bold"
|
||||
onClick={() => setShowNsec(!showNsec)}
|
||||
title={showNsec ? "Hide secret key" : "Show secret key"}
|
||||
>
|
||||
{showNsec ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 px-2 text-xs hover:bg-[#ff0040]/10 text-[#ff0040] font-bold"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(nsec);
|
||||
toast({ title: 'Copied!', description: 'Secret key copied to clipboard' });
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 bg-black rounded border border-[#ff0040]/20 font-mono text-xs break-all text-white">
|
||||
{showNsec ? nsec : '••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••'}
|
||||
</div>
|
||||
<p className="text-xs text-yellow-400">⚠️ This is your PASSWORD - keep it secret and safe!</p>
|
||||
</div>
|
||||
|
||||
{/* npub (public key) */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-bold uppercase text-[#ccff00]">👤 Public Key (npub)</label>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 px-2 text-xs hover:bg-[#ccff00]/10 text-[#ccff00] font-bold"
|
||||
onClick={() => setShowNpub(!showNpub)}
|
||||
title={showNpub ? "Hide public key" : "Show public key"}
|
||||
>
|
||||
{showNpub ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto py-1 px-2 text-xs hover:bg-[#ccff00]/10 text-[#ccff00] font-bold"
|
||||
onClick={() => {
|
||||
try {
|
||||
const { data } = nip19.decode(nsec);
|
||||
const pk = getPublicKey(data as Uint8Array);
|
||||
const npub = nip19.npubEncode(pk);
|
||||
navigator.clipboard.writeText(npub);
|
||||
toast({ title: 'Copied!', description: 'Public key copied to clipboard' });
|
||||
} catch (error) {
|
||||
toast({ title: 'Error', description: 'Failed to copy public key', variant: 'destructive' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 bg-black rounded border border-[#ccff00]/20 font-mono text-xs break-all text-white">
|
||||
{showNpub ? (() => {
|
||||
try {
|
||||
const { data } = nip19.decode(nsec);
|
||||
const pk = getPublicKey(data as Uint8Array);
|
||||
return nip19.npubEncode(pk);
|
||||
} catch {
|
||||
return 'Error generating npub';
|
||||
}
|
||||
})() : '••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••'}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">✓ This is your USERNAME - safe to share publicly</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 text-xs text-gray-300 border-t border-gray-700">
|
||||
<p className="mb-1"><strong>What's the difference?</strong></p>
|
||||
<ul className="space-y-1 ml-4 list-disc">
|
||||
<li><strong className="text-[#ff0040]">nsec</strong> = Your password. Never share this!</li>
|
||||
<li><strong className="text-[#ccff00]">npub</strong> = Your username. Share this with friends!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security options */}
|
||||
<div className='space-y-3'>
|
||||
|
||||
|
||||
<p className="text-sm text-center text-gray-300 font-bold">Now download your secret key to keep it safe:</p>
|
||||
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
{/* Download Option */}
|
||||
<Card className={`cursor-pointer transition-all duration-200 border-2 ${
|
||||
keySecured === 'downloaded'
|
||||
? 'ring-2 ring-green-500 bg-green-950/20 border-green-500/50'
|
||||
: 'hover:bg-[#ff0040]/5 hover:border-[#ff0040]/20 border-[#ff0040]/20'
|
||||
}`}>
|
||||
<CardContent className='p-3'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className='w-full h-auto p-0 justify-start hover:bg-transparent'
|
||||
onClick={downloadKey}
|
||||
>
|
||||
<div className='flex items-center gap-3 w-full'>
|
||||
<div className={`p-1.5 rounded-lg border-2 ${
|
||||
keySecured === 'downloaded'
|
||||
? 'bg-green-900 border-green-500'
|
||||
: 'bg-[#ff0040]/10 border-[#ff0040]/30'
|
||||
}`}>
|
||||
{keySecured === 'downloaded' ? (
|
||||
<CheckCircle className='w-4 h-4 text-green-400' />
|
||||
) : (
|
||||
<Download className='w-4 h-4 text-[#ff0040]' />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1 text-left'>
|
||||
<div className='font-bold text-sm uppercase text-white'>
|
||||
Download as File
|
||||
</div>
|
||||
<div className='text-xs text-gray-400'>
|
||||
Save as nostr-nsec-key.txt file
|
||||
</div>
|
||||
</div>
|
||||
{keySecured === 'downloaded' && (
|
||||
<div className='text-xs font-bold text-green-400 uppercase'>
|
||||
✓ Downloaded
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* Continue button */}
|
||||
<Button
|
||||
className={`w-full rounded-full py-6 text-base font-bold uppercase transform transition-all duration-200 shadow-lg ${
|
||||
keySecured === 'downloaded'
|
||||
? 'bg-[#ccff00] text-black hover:bg-[#ccff00]/90 border-2 border-[#ccff00] shadow-[0_0_20px_rgba(204,255,0,0.5)] hover:scale-105'
|
||||
: 'bg-gray-700 text-gray-400 cursor-not-allowed border-2 border-gray-600'
|
||||
}`}
|
||||
onClick={finishKeySetup}
|
||||
disabled={keySecured !== 'downloaded'}
|
||||
>
|
||||
<LogIn className='w-5 h-5 mr-2 flex-shrink-0' />
|
||||
<span className="text-center leading-tight">
|
||||
{keySecured === 'none' ? (
|
||||
<>
|
||||
Download Your Key First
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
My Key is Safe - Continue
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Profile Step - Optional profile setup */}
|
||||
{step === 'profile' && (
|
||||
<div className='text-center space-y-4'>
|
||||
|
|
@ -658,11 +261,11 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
|
||||
<div className='relative z-10 space-y-2'>
|
||||
<p className='text-base font-semibold'>
|
||||
Almost there! Let's set up your profile
|
||||
You're in! 🛹 Add a name so skaters know who you are
|
||||
</p>
|
||||
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Your profile is your identity on Nostr.
|
||||
This is optional — you can skip it and fill it in later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -759,12 +362,12 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
{isPublishing ? (
|
||||
<>
|
||||
<div className='w-4 h-4 mr-2 border-2 border-current border-t-transparent rounded-full animate-spin' />
|
||||
Creating Profile...
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<User className='w-4 h-4 mr-2' />
|
||||
Create Profile & Finish
|
||||
Save & Start Skating
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -788,50 +391,91 @@ const SignupDialog: React.FC<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Done Step - Account created with signer recommendation */}
|
||||
{/* Done Step - Account created, gentle optional backup */}
|
||||
{step === 'done' && (
|
||||
<div className='py-4 space-y-4'>
|
||||
<Card className='bg-gradient-to-br from-[#ccff00]/10 to-[#00ff00]/10 border-2 border-[#ccff00]/50'>
|
||||
<CardContent className='p-6 text-center space-y-3'>
|
||||
<div className='text-5xl mb-2'>✅</div>
|
||||
<div className='text-5xl mb-2'>🛹</div>
|
||||
<h3 className='text-xl font-black uppercase text-[#ccff00]'>
|
||||
Account Created!
|
||||
You're all set!
|
||||
</h3>
|
||||
<p className='text-gray-300 text-sm'>
|
||||
Your Nostr account is ready to go!
|
||||
Your account is ready. Go find some spots.
|
||||
</p>
|
||||
<CheckCircle className='h-10 w-10 mx-auto text-[#00ff00]' />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Next Steps - Signer Recommendation */}
|
||||
<Card className='bg-blue-950/30 border-2 border-blue-500/30'>
|
||||
{/* Optional backup — no pressure, no jargon */}
|
||||
<Card className='bg-amber-950/20 border-2 border-amber-500/30'>
|
||||
<CardContent className='p-5 space-y-3'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='p-2 rounded-lg bg-blue-500/20'>
|
||||
<Shield className='h-5 w-5 text-blue-400' />
|
||||
<div className='p-2 rounded-lg bg-amber-500/20'>
|
||||
<Shield className='h-5 w-5 text-amber-400' />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className='font-bold text-blue-400'>Recommended: Set Up a Signer App</h4>
|
||||
<p className='text-xs text-gray-400'>Keep your keys extra secure</p>
|
||||
<h4 className='font-bold text-amber-400'>Save a backup (optional)</h4>
|
||||
<p className='text-xs text-gray-400'>So you never lose access to your account</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-sm text-gray-300'>
|
||||
For better security, use a dedicated signer app instead of pasting your secret key everywhere.
|
||||
Signer apps keep your keys safe while letting you use any Nostr app.
|
||||
Your account lives on your device. Save a recovery code somewhere safe and
|
||||
you'll be able to log back in on any device. You can also do this later from Settings.
|
||||
</p>
|
||||
<Link to='/signers' onClick={handleFinishComplete}>
|
||||
<Button className='w-full bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/50'>
|
||||
<ExternalLink className='h-4 w-4 mr-2' />
|
||||
Learn About Signer Apps
|
||||
|
||||
{!showNsec ? (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full border-amber-500/50 text-amber-400 hover:bg-amber-500/10'
|
||||
onClick={() => setShowNsec(true)}
|
||||
>
|
||||
<Key className='h-4 w-4 mr-2' />
|
||||
Show my recovery code
|
||||
</Button>
|
||||
</Link>
|
||||
<button
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
<div className='p-2 bg-black rounded border border-amber-500/20 font-mono text-xs break-all text-white'>
|
||||
{nsec}
|
||||
</div>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='border-amber-500/40 text-amber-300 hover:bg-amber-500/10'
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(nsec);
|
||||
toast({ title: 'Copied!', description: 'Recovery code copied. Keep it somewhere safe.' });
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='border-amber-500/40 text-amber-300 hover:bg-amber-500/10'
|
||||
onClick={downloadKey}
|
||||
>
|
||||
<Download className='h-3.5 w-3.5 mr-1.5' />
|
||||
{keySecured === 'downloaded' ? 'Saved ✓' : 'Download'}
|
||||
</Button>
|
||||
</div>
|
||||
<p className='text-xs text-amber-300/80'>⚠️ Anyone with this code can access your account. Never share it.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className='w-full rounded-full py-3 font-bold uppercase bg-[#ccff00] text-black hover:bg-[#ccff00]/90'
|
||||
onClick={handleFinishComplete}
|
||||
className='w-full text-xs text-gray-500 hover:text-gray-400 py-2'
|
||||
>
|
||||
Skip for now
|
||||
</button>
|
||||
<LogIn className='h-4 w-4 mr-2' />
|
||||
Start Skating
|
||||
</Button>
|
||||
<Link to='/signers' onClick={handleFinishComplete} className='block'>
|
||||
<button className='w-full text-xs text-gray-500 hover:text-gray-400 py-1'>
|
||||
Advanced: use a signer app instead
|
||||
</button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue