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