Signup.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { Checkbox } from '@/components/ui/checkbox'
3 import { Input } from '@/components/ui/input'
4 import { Label } from '@/components/ui/label'
5 import { useNostr } from '@/providers/NostrProvider'
6 import { Check, Copy, Download, RefreshCcw } from 'lucide-react'
7 import { generateSecretKey } from 'nostr-tools'
8 import { nsecEncode } from 'nostr-tools/nip19'
9 import { useState } from 'react'
10 import { useTranslation } from 'react-i18next'
11 import InfoCard from '../InfoCard'
12
13 type Step = 'generate' | 'password'
14
15 export default function Signup({
16 back,
17 onSignupSuccess
18 }: {
19 back: () => void
20 onSignupSuccess: () => void
21 }) {
22 const { t } = useTranslation()
23 const { nsecLogin } = useNostr()
24 const [step, setStep] = useState<Step>('generate')
25 const [nsec, setNsec] = useState(generateNsec())
26 const [checkedSaveKey, setCheckedSaveKey] = useState(false)
27 const [password, setPassword] = useState('')
28 const [confirmPassword, setConfirmPassword] = useState('')
29 const [copied, setCopied] = useState(false)
30
31 const handleDownload = () => {
32 const blob = new Blob([nsec], { type: 'text/plain' })
33 const url = URL.createObjectURL(blob)
34 const a = document.createElement('a')
35 a.href = url
36 a.download = 'nostr-private-key.txt'
37 document.body.appendChild(a)
38 a.click()
39 document.body.removeChild(a)
40 URL.revokeObjectURL(url)
41 }
42
43 const handleSignup = async () => {
44 await nsecLogin(nsec, password || undefined, true)
45 onSignupSuccess()
46 }
47
48 const passwordsMatch = password === confirmPassword
49 const canSubmit = !password || passwordsMatch
50
51 const renderStepIndicator = () => (
52 <div className="flex items-center justify-center gap-2">
53 {(['generate', 'password'] as Step[]).map((s, index) => (
54 <div key={s} className="flex items-center">
55 <div
56 className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
57 step === s
58 ? 'bg-primary text-primary-foreground'
59 : step === 'password' && s === 'generate'
60 ? 'bg-primary/20 text-primary'
61 : 'bg-muted text-muted-foreground'
62 }`}
63 >
64 {index + 1}
65 </div>
66 {index < 1 && <div className="w-12 h-0.5 bg-muted mx-1" />}
67 </div>
68 ))}
69 </div>
70 )
71
72 if (step === 'generate') {
73 return (
74 <div className="space-y-6">
75 {renderStepIndicator()}
76
77 <div className="text-center">
78 <h3 className="text-lg font-semibold mb-2">{t('Create Your Nostr Account')}</h3>
79 <p className="text-sm text-muted-foreground">
80 {t('Generate your unique private key. This is your digital identity.')}
81 </p>
82 </div>
83
84 <InfoCard
85 variant="alert"
86 title={t('Critical: Save Your Private Key')}
87 content={t(
88 'Your private key IS your account. There is no password recovery. If you lose it, you lose your account forever. Please save it in a secure location.'
89 )}
90 />
91
92 <div className="space-y-1">
93 <Label>{t('Your Private Key')}</Label>
94 <div className="flex gap-2">
95 <Input
96 value={nsec}
97 readOnly
98 className="font-mono text-sm"
99 onClick={(e) => e.currentTarget.select()}
100 />
101 <Button
102 type="button"
103 variant="secondary"
104 size="icon"
105 onClick={() => setNsec(generateNsec())}
106 title={t('Generate new key')}
107 >
108 <RefreshCcw />
109 </Button>
110 </div>
111 </div>
112
113 <div className="w-full flex flex-wrap gap-2">
114 <Button onClick={handleDownload} className="flex-1">
115 <Download />
116 {t('Download Backup File')}
117 </Button>
118 <Button
119 onClick={() => {
120 navigator.clipboard.writeText(nsec)
121 setCopied(true)
122 setTimeout(() => setCopied(false), 2000)
123 }}
124 variant="secondary"
125 className="flex-1"
126 >
127 {copied ? <Check /> : <Copy />}
128 {copied ? t('Copied to Clipboard') : t('Copy to Clipboard')}
129 </Button>
130 </div>
131
132 <div className="flex items-center gap-2 ml-2">
133 <Checkbox
134 id="acknowledge-checkbox"
135 checked={checkedSaveKey}
136 onCheckedChange={(c) => setCheckedSaveKey(!!c)}
137 />
138 <Label htmlFor="acknowledge-checkbox" className="cursor-pointer">
139 {t('I have safely backed up my private key')}
140 </Label>
141 </div>
142
143 <div className="flex gap-2">
144 <Button variant="secondary" onClick={back} className="w-fit px-6">
145 {t('Back')}
146 </Button>
147
148 <Button onClick={() => setStep('password')} className="flex-1" disabled={!checkedSaveKey}>
149 {t('Continue')}
150 </Button>
151 </div>
152 </div>
153 )
154 }
155
156 // step === 'password'
157 return (
158 <div className="space-y-6">
159 {renderStepIndicator()}
160
161 <div className="text-center">
162 <h3 className="text-lg font-semibold mb-2">{t('Secure Your Account')}</h3>
163 <p className="text-sm text-muted-foreground">
164 {t('Add an extra layer of protection with a password')}
165 </p>
166 </div>
167
168 <InfoCard
169 title={t('Password Protection (Recommended)')}
170 content={t(
171 'Add a password to encrypt your private key in this browser. This is optional but strongly recommended for better security.'
172 )}
173 />
174
175 <div className="space-y-2">
176 <div className="space-y-1">
177 <Label htmlFor="password-input">{t('Password (Optional)')}</Label>
178 <Input
179 id="password-input"
180 type="password"
181 placeholder={t('Create a password (or skip)')}
182 value={password}
183 onChange={(e) => setPassword(e.target.value)}
184 />
185 </div>
186
187 {password && (
188 <div className="space-y-1">
189 <Label htmlFor="confirm-password-input">{t('Confirm Password')}</Label>
190 <Input
191 id="confirm-password-input"
192 type="password"
193 placeholder={t('Enter your password again')}
194 value={confirmPassword}
195 onChange={(e) => setConfirmPassword(e.target.value)}
196 />
197 {confirmPassword && !passwordsMatch && (
198 <p className="text-xs text-red-500">{t('Passwords do not match')}</p>
199 )}
200 </div>
201 )}
202 </div>
203
204 <div className="w-full flex gap-2">
205 <Button
206 variant="secondary"
207 onClick={() => {
208 setStep('generate')
209 setPassword('')
210 setConfirmPassword('')
211 }}
212 className="w-fit px-6"
213 >
214 {t('Back')}
215 </Button>
216 <Button onClick={handleSignup} className="flex-1" disabled={!canSubmit}>
217 {t('Complete Signup')}
218 </Button>
219 </div>
220 </div>
221 )
222 }
223
224 function generateNsec() {
225 const sk = generateSecretKey()
226 return nsecEncode(sk)
227 }
228