mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-27 13:09:22 +00:00
try to mimic a little closer to surveil
This commit is contained in:
parent
4734e96fe7
commit
4d5df6535f
@ -1,7 +1,4 @@
|
|||||||
// NOTE: This file is stable and usually should not be modified.
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
// 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 { Shield, Upload } from 'lucide-react';
|
import { Shield, Upload } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button.tsx';
|
import { Button } from '@/components/ui/button.tsx';
|
||||||
import { Input } from '@/components/ui/input.tsx';
|
import { Input } from '@/components/ui/input.tsx';
|
||||||
@ -14,7 +11,8 @@ import {
|
|||||||
} from '@/components/ui/dialog.tsx';
|
} from '@/components/ui/dialog.tsx';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.tsx';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.tsx';
|
||||||
import { useLoginActions } from '@/hooks/useLoginActions';
|
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||||
import SignupDialog from './SignupDialog';
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import SignupFlow from './SignupFlow';
|
||||||
|
|
||||||
interface LoginDialogProps {
|
interface LoginDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -26,9 +24,19 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [nsec, setNsec] = useState('');
|
const [nsec, setNsec] = useState('');
|
||||||
const [bunkerUri, setBunkerUri] = useState('');
|
const [bunkerUri, setBunkerUri] = useState('');
|
||||||
const [isSignupOpen, setIsSignupOpen] = useState(false);
|
const [view, setView] = useState<'login' | 'signup'>('login');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const login = useLoginActions();
|
const login = useLoginActions();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setView('login');
|
||||||
|
setIsLoading(false);
|
||||||
|
setNsec('');
|
||||||
|
setBunkerUri('');
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleExtensionLogin = () => {
|
const handleExtensionLogin = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -41,6 +49,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Extension login failed:', error);
|
console.error('Extension login failed:', error);
|
||||||
|
toast({ title: 'Login Failed', description: (error as Error).message, variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -49,13 +58,13 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
const handleKeyLogin = () => {
|
const handleKeyLogin = () => {
|
||||||
if (!nsec.trim()) return;
|
if (!nsec.trim()) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
login.nsec(nsec);
|
login.nsec(nsec);
|
||||||
onLogin();
|
onLogin();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Nsec login failed:', error);
|
console.error('Nsec login failed:', error);
|
||||||
|
toast({ title: 'Login Failed', description: 'Invalid nsec key.', variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -64,13 +73,13 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
const handleBunkerLogin = () => {
|
const handleBunkerLogin = () => {
|
||||||
if (!bunkerUri.trim() || !bunkerUri.startsWith('bunker://')) return;
|
if (!bunkerUri.trim() || !bunkerUri.startsWith('bunker://')) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
login.bunker(bunkerUri);
|
login.bunker(bunkerUri);
|
||||||
onLogin();
|
onLogin();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Bunker login failed:', error);
|
console.error('Bunker login failed:', error);
|
||||||
|
toast({ title: 'Bunker Login Failed', description: 'Could not connect to bunker.', variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -79,7 +88,6 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const content = event.target?.result as string;
|
const content = event.target?.result as string;
|
||||||
@ -88,143 +96,103 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin }) =
|
|||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSignupClick = () => {
|
const renderLoginView = () => (
|
||||||
onClose();
|
|
||||||
setIsSignupOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
<>
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<DialogHeader className='px-6 pt-6 pb-0 relative'>
|
||||||
<DialogContent className='sm:max-w-md p-0 overflow-hidden rounded-2xl'>
|
<DialogTitle className='text-xl font-semibold text-center'>Log in</DialogTitle>
|
||||||
<DialogHeader className='px-6 pt-6 pb-0 relative'>
|
<DialogDescription className='text-center text-muted-foreground mt-2'>
|
||||||
<DialogTitle className='text-xl font-semibold text-center'>Log in</DialogTitle>
|
Access your account securely with your preferred method
|
||||||
<DialogDescription className='text-center text-muted-foreground mt-2'>
|
</DialogDescription>
|
||||||
Access your account securely with your preferred method
|
</DialogHeader>
|
||||||
</DialogDescription>
|
<div className='px-6 py-8 space-y-6'>
|
||||||
</DialogHeader>
|
<div className='text-center'>
|
||||||
|
<p className='text-sm text-muted-foreground'>New to Nostr?</p>
|
||||||
<div className='px-6 py-8 space-y-6'>
|
<Button variant='link' className='text-primary' onClick={() => setView('signup')}>
|
||||||
<Tabs defaultValue={'nostr' in window ? 'extension' : 'key'} className='w-full'>
|
Create an account
|
||||||
<TabsList className='grid grid-cols-3 mb-6'>
|
</Button>
|
||||||
<TabsTrigger value='extension'>Extension</TabsTrigger>
|
</div>
|
||||||
<TabsTrigger value='key'>Nsec</TabsTrigger>
|
<div className='relative'>
|
||||||
<TabsTrigger value='bunker'>Bunker</TabsTrigger>
|
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
||||||
</TabsList>
|
<div className='w-full border-t border-border' />
|
||||||
|
</div>
|
||||||
<TabsContent value='extension' className='space-y-4'>
|
<div className='relative flex justify-center text-sm'>
|
||||||
<div className='text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800'>
|
<span className='px-2 bg-background text-muted-foreground'>Or log in with</span>
|
||||||
<Shield className='w-12 h-12 mx-auto mb-3 text-primary' />
|
</div>
|
||||||
<p className='text-sm text-gray-600 dark:text-gray-300 mb-4'>
|
</div>
|
||||||
Login with one click using the browser extension
|
<Tabs defaultValue={'nostr' in window ? 'extension' : 'key'} className='w-full'>
|
||||||
</p>
|
<TabsList className='grid grid-cols-3 mb-6'>
|
||||||
<Button
|
<TabsTrigger value='extension'>Extension</TabsTrigger>
|
||||||
className='w-full rounded-full py-6'
|
<TabsTrigger value='key'>Nsec</TabsTrigger>
|
||||||
onClick={handleExtensionLogin}
|
<TabsTrigger value='bunker'>Bunker</TabsTrigger>
|
||||||
disabled={isLoading}
|
</TabsList>
|
||||||
>
|
<TabsContent value='extension' className='space-y-4'>
|
||||||
{isLoading ? 'Logging in...' : 'Login with Extension'}
|
<div className='text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800'>
|
||||||
</Button>
|
<Shield className='w-12 h-12 mx-auto mb-3 text-primary' />
|
||||||
</div>
|
<p className='text-sm text-gray-600 dark:text-gray-300 mb-4'>
|
||||||
</TabsContent>
|
Login with one click using the browser extension
|
||||||
|
</p>
|
||||||
<TabsContent value='key' className='space-y-4'>
|
<Button className='w-full rounded-full py-6' onClick={handleExtensionLogin} disabled={isLoading}>
|
||||||
<div className='space-y-4'>
|
{isLoading ? 'Logging in...' : 'Login with Extension'}
|
||||||
<div className='space-y-2'>
|
|
||||||
<label htmlFor='nsec' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
|
||||||
Enter your nsec
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id='nsec'
|
|
||||||
value={nsec}
|
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
|
||||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
|
||||||
placeholder='nsec1...'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='text-center'>
|
|
||||||
<p className='text-sm mb-2 text-gray-600 dark:text-gray-400'>Or upload a key file</p>
|
|
||||||
<input
|
|
||||||
type='file'
|
|
||||||
accept='.txt'
|
|
||||||
className='hidden'
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700'
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<Upload className='w-4 h-4 mr-2' />
|
|
||||||
Upload Nsec File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className='w-full rounded-full py-6 mt-4'
|
|
||||||
onClick={handleKeyLogin}
|
|
||||||
disabled={isLoading || !nsec.trim()}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Verifying...' : 'Login with Nsec'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value='bunker' className='space-y-4'>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<label htmlFor='bunkerUri' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
|
||||||
Bunker URI
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id='bunkerUri'
|
|
||||||
value={bunkerUri}
|
|
||||||
onChange={(e) => setBunkerUri(e.target.value)}
|
|
||||||
className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary'
|
|
||||||
placeholder='bunker://'
|
|
||||||
/>
|
|
||||||
{bunkerUri && !bunkerUri.startsWith('bunker://') && (
|
|
||||||
<p className='text-red-500 text-xs'>URI must start with bunker://</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className='w-full rounded-full py-6'
|
|
||||||
onClick={handleBunkerLogin}
|
|
||||||
disabled={isLoading || !bunkerUri.trim() || !bunkerUri.startsWith('bunker://')}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Connecting...' : 'Login with Bunker'}
|
|
||||||
</Button>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<div className='relative mt-6'>
|
|
||||||
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
|
||||||
<div className='w-full border-t border-border' />
|
|
||||||
</div>
|
|
||||||
<div className='relative flex justify-center text-sm'>
|
|
||||||
<span className='px-2 bg-background text-muted-foreground'>
|
|
||||||
New to Nostr?
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full'
|
|
||||||
onClick={handleSignupClick}
|
|
||||||
>
|
|
||||||
Create an account
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</DialogContent>
|
<TabsContent value='key' className='space-y-4'>
|
||||||
</Dialog>
|
<div className='space-y-4'>
|
||||||
<SignupDialog isOpen={isSignupOpen} onClose={() => setIsSignupOpen(false)} />
|
<div className='space-y-2'>
|
||||||
|
<label htmlFor='nsec' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||||
|
Enter your nsec
|
||||||
|
</label>
|
||||||
|
<Input id='nsec' value={nsec} onChange={(e) => setNsec(e.target.value)} className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' placeholder='nsec1...' />
|
||||||
|
</div>
|
||||||
|
<div className='text-center'>
|
||||||
|
<p className='text-sm mb-2 text-gray-600 dark:text-gray-400'>Or upload a key file</p>
|
||||||
|
<input type='file' accept='.txt' className='hidden' ref={fileInputRef} onChange={handleFileUpload} />
|
||||||
|
<Button variant='outline' className='w-full dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700' onClick={() => fileInputRef.current?.click()}>
|
||||||
|
<Upload className='w-4 h-4 mr-2' />
|
||||||
|
Upload Nsec File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button className='w-full rounded-full py-6 mt-4' onClick={handleKeyLogin} disabled={isLoading || !nsec.trim()}>
|
||||||
|
{isLoading ? 'Verifying...' : 'Login with Nsec'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='bunker' className='space-y-4'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<label htmlFor='bunkerUri' className='text-sm font-medium text-gray-700 dark:text-gray-400'>
|
||||||
|
Bunker URI
|
||||||
|
</label>
|
||||||
|
<Input id='bunkerUri' value={bunkerUri} onChange={(e) => setBunkerUri(e.target.value)} className='rounded-lg border-gray-300 dark:border-gray-700 focus-visible:ring-primary' placeholder='bunker://' />
|
||||||
|
{bunkerUri && !bunkerUri.startsWith('bunker://') && (<p className='text-red-500 text-xs'>URI must start with bunker://</p>)}
|
||||||
|
</div>
|
||||||
|
<Button className='w-full rounded-full py-6' onClick={handleBunkerLogin} disabled={isLoading || !bunkerUri.trim() || !bunkerUri.startsWith('bunker://')}>
|
||||||
|
{isLoading ? 'Connecting...' : 'Login with Bunker'}
|
||||||
|
</Button>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderSignupView = () => (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Account</DialogTitle>
|
||||||
|
<DialogDescription>Create a new Nostr account.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className='px-6 py-8'>
|
||||||
|
<SignupFlow onBack={() => setView('login')} onClose={onClose} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className='sm:max-w-md p-0 overflow-hidden rounded-2xl'>
|
||||||
|
{view === 'login' ? renderLoginView() : renderSignupView()}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginDialog;
|
export default LoginDialog;
|
||||||
|
@ -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<SignupDialogProps> = ({ 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<string, string> = {};
|
|
||||||
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 (
|
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{getTitle()}</DialogTitle>
|
|
||||||
<DialogDescription>{getDescription()}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{step === 'welcome' && (
|
|
||||||
<div className='text-center space-y-4'>
|
|
||||||
<p>
|
|
||||||
Your key is your identity. It's how you log in and how others see you.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
className='w-full'
|
|
||||||
onClick={() => setStep('generate')}
|
|
||||||
>
|
|
||||||
<ArrowRight className='w-5 h-5 mr-2' />
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'generate' && (
|
|
||||||
<div className='text-center space-y-4'>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className='space-y-3'>
|
|
||||||
<div className='relative'>
|
|
||||||
<KeyRound className='w-20 h-20 mx-auto animate-pulse' />
|
|
||||||
</div>
|
|
||||||
<p className='text-lg font-semibold'>
|
|
||||||
Generating your key...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='space-y-3'>
|
|
||||||
<KeyRound className='w-20 h-20 mx-auto' />
|
|
||||||
<p className='text-lg font-semibold'>
|
|
||||||
Ready to generate your key?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isLoading && (
|
|
||||||
<Button
|
|
||||||
className='w-full'
|
|
||||||
onClick={generateKey}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Generate My Key
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'download' && (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div className='relative p-3 bg-muted rounded-xl border'>
|
|
||||||
<div className='flex items-center gap-2 mb-2'>
|
|
||||||
<Lock className='w-4 h-4' />
|
|
||||||
<span className='text-sm font-medium'>
|
|
||||||
Your Secret Key
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className='p-2 bg-background rounded-lg border'>
|
|
||||||
<code className='text-xs break-all font-mono'>{nsec}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-3'>
|
|
||||||
<p className='text-sm font-medium text-center'>
|
|
||||||
Choose how to secure your key:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-2'>
|
|
||||||
<Card className={`cursor-pointer transition-all duration-200 ${
|
|
||||||
keySecured === 'copied'
|
|
||||||
? 'ring-2 ring-primary'
|
|
||||||
: 'hover:bg-muted'
|
|
||||||
}`}>
|
|
||||||
<CardContent className='p-3'>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className='w-full h-auto p-0 justify-start'
|
|
||||||
onClick={copyKey}
|
|
||||||
>
|
|
||||||
<div className='flex items-center gap-3 w-full'>
|
|
||||||
<div className={`p-1.5 rounded-lg ${
|
|
||||||
keySecured === 'copied'
|
|
||||||
? 'bg-primary/20'
|
|
||||||
: 'bg-muted'
|
|
||||||
}`}>
|
|
||||||
{keySecured === 'copied' ? (
|
|
||||||
<CheckCircle className='w-4 h-4 text-primary' />
|
|
||||||
) : (
|
|
||||||
<Copy className='w-4 h-4' />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 text-left'>
|
|
||||||
<div className='font-medium text-sm'>
|
|
||||||
Copy to Clipboard
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{keySecured === 'copied' && (
|
|
||||||
<div className='text-xs font-medium text-primary'>
|
|
||||||
✓ Copied
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className={`cursor-pointer transition-all duration-200 ${
|
|
||||||
keySecured === 'downloaded'
|
|
||||||
? 'ring-2 ring-primary'
|
|
||||||
: 'hover:bg-muted'
|
|
||||||
}`}>
|
|
||||||
<CardContent className='p-3'>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className='w-full h-auto p-0 justify-start'
|
|
||||||
onClick={downloadKey}
|
|
||||||
>
|
|
||||||
<div className='flex items-center gap-3 w-full'>
|
|
||||||
<div className={`p-1.5 rounded-lg ${
|
|
||||||
keySecured === 'downloaded'
|
|
||||||
? 'bg-primary/20'
|
|
||||||
: 'bg-muted'
|
|
||||||
}`}>
|
|
||||||
{keySecured === 'downloaded' ? (
|
|
||||||
<CheckCircle className='w-4 h-4 text-primary' />
|
|
||||||
) : (
|
|
||||||
<Download className='w-4 h-4' />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='flex-1 text-left'>
|
|
||||||
<div className='font-medium text-sm'>
|
|
||||||
Download as File
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{keySecured === 'downloaded' && (
|
|
||||||
<div className='text-xs font-medium text-primary'>
|
|
||||||
✓ Downloaded
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className='w-full'
|
|
||||||
onClick={finishKeySetup}
|
|
||||||
disabled={keySecured === 'none'}
|
|
||||||
>
|
|
||||||
<Lock className='w-4 h-4 mr-2' />
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'profile' && (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<div className={`space-y-4 text-left ${isPublishing ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<label htmlFor='profile-name' className='text-sm font-medium'>
|
|
||||||
Display Name
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id='profile-name'
|
|
||||||
value={profileData.name}
|
|
||||||
onChange={(e) => setProfileData(prev => ({ ...prev, name: e.target.value }))}
|
|
||||||
placeholder='Your name'
|
|
||||||
disabled={isPublishing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-2'>
|
|
||||||
<label htmlFor='profile-about' className='text-sm font-medium'>
|
|
||||||
Bio
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
id='profile-about'
|
|
||||||
value={profileData.about}
|
|
||||||
onChange={(e) => setProfileData(prev => ({ ...prev, about: e.target.value }))}
|
|
||||||
placeholder='Tell others about yourself...'
|
|
||||||
rows={3}
|
|
||||||
disabled={isPublishing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='space-y-3'>
|
|
||||||
<Button
|
|
||||||
className='w-full'
|
|
||||||
onClick={() => finishSignup(false)}
|
|
||||||
disabled={isPublishing}
|
|
||||||
>
|
|
||||||
{isPublishing ? 'Creating Profile...' : 'Create Profile & Finish'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full'
|
|
||||||
onClick={() => finishSignup(true)}
|
|
||||||
disabled={isPublishing}
|
|
||||||
>
|
|
||||||
Skip for now
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'done' && (
|
|
||||||
<div className='flex justify-center items-center py-8'>
|
|
||||||
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-primary'></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignupDialog;
|
|
212
src/components/auth/SignupFlow.tsx
Normal file
212
src/components/auth/SignupFlow.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
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 { 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 SignupFlowProps {
|
||||||
|
onBack: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignupFlow: React.FC<SignupFlowProps> = ({ onBack, onClose }) => {
|
||||||
|
const [signupStep, setSignupStep] = 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();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSignupStep('welcome');
|
||||||
|
setIsLoading(false);
|
||||||
|
setNsec('');
|
||||||
|
setKeySecured('none');
|
||||||
|
setProfileData({ name: '', about: '' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generateKey = () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const sk = generateSecretKey();
|
||||||
|
setNsec(nip19.nsecEncode(sk));
|
||||||
|
setSignupStep('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);
|
||||||
|
setSignupStep('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<string, string> = {};
|
||||||
|
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.' });
|
||||||
|
}
|
||||||
|
setSignupStep('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' });
|
||||||
|
setSignupStep('done');
|
||||||
|
setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
toast({ title: 'Welcome!', description: 'You are now logged in.' });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{signupStep === 'welcome' && (
|
||||||
|
<div className='text-center space-y-4'>
|
||||||
|
<div className='flex justify-center items-center h-24'>
|
||||||
|
<KeyRound className='w-16 h-16 text-primary' />
|
||||||
|
</div>
|
||||||
|
<p>Your key is your identity. It's how you log in and how others see you.</p>
|
||||||
|
<Button className='w-full' onClick={() => setSignupStep('generate')}>
|
||||||
|
<ArrowRight className='w-5 h-5 mr-2' />
|
||||||
|
Get Started
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{signupStep === 'generate' && (
|
||||||
|
<div className='text-center space-y-4'>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<div className='relative flex justify-center items-center h-24'>
|
||||||
|
<KeyRound className='w-20 h-20 mx-auto animate-pulse text-primary' />
|
||||||
|
</div>
|
||||||
|
<p className='text-lg font-semibold'>Generating your key...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<div className='flex justify-center items-center h-24'>
|
||||||
|
<KeyRound className='w-16 h-16 mx-auto text-primary' />
|
||||||
|
</div>
|
||||||
|
<p className='text-lg font-semibold'>Ready to generate your key?</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && (<Button className='w-full' onClick={generateKey} disabled={isLoading}>Generate My Key</Button>)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{signupStep === 'download' && (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='relative p-3 bg-muted rounded-xl border'>
|
||||||
|
<div className='flex items-center gap-2 mb-2'><Lock className='w-4 h-4' /><span className='text-sm font-medium'>Your Secret Key</span></div>
|
||||||
|
<div className='p-2 bg-background rounded-lg border'><code className='text-xs break-all font-mono'>{nsec}</code></div>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<p className='text-sm font-medium text-center'>Choose how to secure your key:</p>
|
||||||
|
<div className='grid grid-cols-1 gap-2'>
|
||||||
|
<Card className={`cursor-pointer transition-all duration-200 ${keySecured === 'copied' ? 'ring-2 ring-primary' : 'hover:bg-muted'}`}>
|
||||||
|
<CardContent className='p-3'>
|
||||||
|
<Button variant="ghost" className='w-full h-auto p-0 justify-start' onClick={copyKey}>
|
||||||
|
<div className='flex items-center gap-3 w-full'>
|
||||||
|
<div className={`p-1.5 rounded-lg ${keySecured === 'copied' ? 'bg-primary/20' : 'bg-muted'}`}>
|
||||||
|
{keySecured === 'copied' ? (<CheckCircle className='w-4 h-4 text-primary' />) : (<Copy className='w-4 h-4' />)}
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 text-left'><div className='font-medium text-sm'>Copy to Clipboard</div></div>
|
||||||
|
{keySecured === 'copied' && (<div className='text-xs font-medium text-primary'>✓ Copied</div>)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className={`cursor-pointer transition-all duration-200 ${keySecured === 'downloaded' ? 'ring-2 ring-primary' : 'hover:bg-muted'}`}>
|
||||||
|
<CardContent className='p-3'>
|
||||||
|
<Button variant="ghost" className='w-full h-auto p-0 justify-start' onClick={downloadKey}>
|
||||||
|
<div className='flex items-center gap-3 w-full'>
|
||||||
|
<div className={`p-1.5 rounded-lg ${keySecured === 'downloaded' ? 'bg-primary/20' : 'bg-muted'}`}>
|
||||||
|
{keySecured === 'downloaded' ? (<CheckCircle className='w-4 h-4 text-primary' />) : (<Download className='w-4 h-4' />)}
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 text-left'><div className='font-medium text-sm'>Download as File</div></div>
|
||||||
|
{keySecured === 'downloaded' && (<div className='text-xs font-medium text-primary'>✓ Downloaded</div>)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Button className='w-full' onClick={finishKeySetup} disabled={keySecured === 'none'}><Lock className='w-4 h-4 mr-2' />Continue</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{signupStep === 'profile' && (
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className={`space-y-4 text-left ${isPublishing ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<label htmlFor='profile-name' className='text-sm font-medium'>Display Name</label>
|
||||||
|
<Input id='profile-name' value={profileData.name} onChange={(e) => setProfileData(prev => ({ ...prev, name: e.target.value }))} placeholder='Your name' disabled={isPublishing} />
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<label htmlFor='profile-about' className='text-sm font-medium'>Bio</label>
|
||||||
|
<Textarea id='profile-about' value={profileData.about} onChange={(e) => setProfileData(prev => ({ ...prev, about: e.target.value }))} placeholder='Tell others about yourself...' rows={3} disabled={isPublishing} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<Button className='w-full' onClick={() => finishSignup(false)} disabled={isPublishing}>{isPublishing ? 'Creating Profile...' : 'Create Profile & Finish'}</Button>
|
||||||
|
<Button variant='outline' className='w-full' onClick={() => finishSignup(true)} disabled={isPublishing}>Skip for now</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{signupStep === 'done' && (
|
||||||
|
<div className='flex justify-center items-center py-8'>
|
||||||
|
<div className='animate-spin rounded-full h-12 w-12 border-b-2 border-primary'></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button variant='link' onClick={onBack}>Back to login</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupFlow;
|
Loading…
x
Reference in New Issue
Block a user