signup
This commit is contained in:
229
src/components/AccountManager/Signup.tsx
Normal file
229
src/components/AccountManager/Signup.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { ArrowLeft, Check, Copy, Download, RefreshCcw } from 'lucide-react'
|
||||
import { generateSecretKey } from 'nostr-tools'
|
||||
import { nsecEncode } from 'nostr-tools/nip19'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfoCard from '../InfoCard'
|
||||
|
||||
type Step = 'generate' | 'password'
|
||||
|
||||
export default function Signup({
|
||||
back,
|
||||
onSignupSuccess
|
||||
}: {
|
||||
back: () => void
|
||||
onSignupSuccess: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { nsecLogin } = useNostr()
|
||||
const [step, setStep] = useState<Step>('generate')
|
||||
const [nsec, setNsec] = useState(generateNsec())
|
||||
const [checkedSaveKey, setCheckedSaveKey] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([nsec], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'nostr-private-key.txt'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleSignup = async () => {
|
||||
await nsecLogin(nsec, password || undefined, true)
|
||||
onSignupSuccess()
|
||||
}
|
||||
|
||||
const passwordsMatch = password === confirmPassword
|
||||
const canSubmit = !password || passwordsMatch
|
||||
|
||||
const renderStepIndicator = () => (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{(['generate', 'password'] as Step[]).map((s, index) => (
|
||||
<div key={s} className="flex items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
|
||||
step === s
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: step === 'password' && s === 'generate'
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
{index < 1 && <div className="w-12 h-0.5 bg-muted mx-1" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (step === 'generate') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold mb-2">{t('Generate Your Account')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('Your private key IS your account. Keep it safe!')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InfoCard
|
||||
variant="alert"
|
||||
title={t('Important')}
|
||||
content={t(
|
||||
'In Nostr, your private key IS your account. If you lose your private key, you lose your account forever.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label>{t('Your Private Key')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={nsec}
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
onClick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => setNsec(generateNsec())}
|
||||
title={t('Generate new key')}
|
||||
>
|
||||
<RefreshCcw />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex gap-2 items-center">
|
||||
<Button onClick={handleDownload} className="w-full">
|
||||
<Download />
|
||||
{t('Download Backup File')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(nsec)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
{copied ? <Check /> : <Copy />}
|
||||
{copied ? t('Copied to Clipboard') : t('Copy to Clipboard')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
<Checkbox
|
||||
id="acknowledge-checkbox"
|
||||
checked={checkedSaveKey}
|
||||
onCheckedChange={(c) => setCheckedSaveKey(!!c)}
|
||||
/>
|
||||
<Label htmlFor="acknowledge-checkbox" className="cursor-pointer">
|
||||
{t('I already saved my private key securely.')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={back} className="w-fit px-6">
|
||||
<ArrowLeft />
|
||||
{t('Back')}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => setStep('password')} className="flex-1" disabled={!checkedSaveKey}>
|
||||
{t('Continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// step === 'password'
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{renderStepIndicator()}
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold mb-2">{t('Almost Done!')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('Set a password to encrypt your key, or skip to finish')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InfoCard
|
||||
title={t('Password Protection (Optional)')}
|
||||
content={t(
|
||||
'Setting a password encrypts your private key in this browser. You can skip this step, but we recommend setting one for added security.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="password-input">{t('Password (Optional)')}</Label>
|
||||
<Input
|
||||
id="password-input"
|
||||
type="password"
|
||||
placeholder={t('Enter password or leave empty to skip')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{password && (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="confirm-password-input">{t('Confirm Password')}</Label>
|
||||
<Input
|
||||
id="confirm-password-input"
|
||||
type="password"
|
||||
placeholder={t('Re-enter password')}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
{confirmPassword && !passwordsMatch && (
|
||||
<p className="text-xs text-red-500">{t('Passwords do not match')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStep('generate')
|
||||
setPassword('')
|
||||
setConfirmPassword('')
|
||||
}}
|
||||
className="w-fit px-6"
|
||||
>
|
||||
<ArrowLeft />
|
||||
{t('Back')}
|
||||
</Button>
|
||||
<Button onClick={handleSignup} className="flex-1" disabled={!canSubmit}>
|
||||
{t('Finish Signup')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function generateNsec() {
|
||||
const sk = generateSecretKey()
|
||||
return nsecEncode(sk)
|
||||
}
|
||||
Reference in New Issue
Block a user