refactor: password login dialog

This commit is contained in:
codytseng 2025-12-26 20:25:28 +08:00
parent e60a460480
commit 6f64aafdae
20 changed files with 194 additions and 29 deletions

View file

@ -0,0 +1,81 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
type PasswordInputDialogProps = {
open: boolean
title?: string
description?: string
onConfirm: (password: string) => void
onCancel: () => void
}
export default function PasswordInputDialog({
open,
title,
description,
onConfirm,
onCancel
}: PasswordInputDialogProps) {
const { t } = useTranslation()
const [password, setPassword] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (open) {
setPassword('')
// Focus input after dialog opens
setTimeout(() => {
inputRef.current?.focus()
}, 100)
}
}, [open])
const handleConfirm = () => {
if (password) {
onConfirm(password)
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && password) {
handleConfirm()
}
}
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title || t('Enter Password')}</DialogTitle>
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
<Input
ref={inputRef}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('Password')}
/>
<DialogFooter className="w-full flex flex-wrap gap-2">
<Button variant="outline" onClick={onCancel} className="flex-1">
{t('Cancel')}
</Button>
<Button onClick={handleConfirm} disabled={!password} className="flex-1">
{t('Confirm')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View file

@ -634,6 +634,9 @@ export default {
'Create a password (or skip)': 'أنشئ كلمة مرور (أو تخطى)', 'Create a password (or skip)': 'أنشئ كلمة مرور (أو تخطى)',
'Enter your password again': 'أدخل كلمة المرور مرة أخرى', 'Enter your password again': 'أدخل كلمة المرور مرة أخرى',
'Complete Signup': 'إكمال التسجيل', 'Complete Signup': 'إكمال التسجيل',
Recommended: 'موصى به' Recommended: 'موصى به',
'Enter Password': 'أدخل كلمة المرور',
Password: 'كلمة المرور',
Confirm: 'تأكيد'
} }
} }

View file

@ -655,6 +655,9 @@ export default {
'Create a password (or skip)': 'Erstellen Sie ein Passwort (oder überspringen)', 'Create a password (or skip)': 'Erstellen Sie ein Passwort (oder überspringen)',
'Enter your password again': 'Geben Sie Ihr Passwort erneut ein', 'Enter your password again': 'Geben Sie Ihr Passwort erneut ein',
'Complete Signup': 'Registrierung abschließen', 'Complete Signup': 'Registrierung abschließen',
Recommended: 'Empfohlen' Recommended: 'Empfohlen',
'Enter Password': 'Passwort eingeben',
Password: 'Passwort',
Confirm: 'Bestätigen'
} }
} }

View file

@ -639,6 +639,9 @@ export default {
'Create a password (or skip)': 'Create a password (or skip)', 'Create a password (or skip)': 'Create a password (or skip)',
'Enter your password again': 'Enter your password again', 'Enter your password again': 'Enter your password again',
'Complete Signup': 'Complete Signup', 'Complete Signup': 'Complete Signup',
Recommended: 'Recommended' Recommended: 'Recommended',
'Enter Password': 'Enter Password',
Password: 'Password',
Confirm: 'Confirm'
} }
} }

View file

@ -649,6 +649,9 @@ export default {
'Create a password (or skip)': 'Crear una contraseña (o saltar)', 'Create a password (or skip)': 'Crear una contraseña (o saltar)',
'Enter your password again': 'Ingresa tu contraseña nuevamente', 'Enter your password again': 'Ingresa tu contraseña nuevamente',
'Complete Signup': 'Completar registro', 'Complete Signup': 'Completar registro',
Recommended: 'Recomendado' Recommended: 'Recomendado',
'Enter Password': 'Ingresar contraseña',
Password: 'Contraseña',
Confirm: 'Confirmar'
} }
} }

View file

@ -644,6 +644,9 @@ export default {
'Create a password (or skip)': 'یک رمز عبور ایجاد کنید (یا رد کنید)', 'Create a password (or skip)': 'یک رمز عبور ایجاد کنید (یا رد کنید)',
'Enter your password again': 'رمز عبور خود را دوباره وارد کنید', 'Enter your password again': 'رمز عبور خود را دوباره وارد کنید',
'Complete Signup': 'تکمیل ثبت‌نام', 'Complete Signup': 'تکمیل ثبت‌نام',
Recommended: 'توصیه شده' Recommended: 'توصیه شده',
'Enter Password': 'رمز عبور را وارد کنید',
Password: 'رمز عبور',
Confirm: 'تأیید'
} }
} }

View file

@ -652,6 +652,9 @@ export default {
'Create a password (or skip)': 'Créez un mot de passe (ou ignorez)', 'Create a password (or skip)': 'Créez un mot de passe (ou ignorez)',
'Enter your password again': 'Entrez à nouveau votre mot de passe', 'Enter your password again': 'Entrez à nouveau votre mot de passe',
'Complete Signup': "Terminer l'inscription", 'Complete Signup': "Terminer l'inscription",
Recommended: 'Recommandé' Recommended: 'Recommandé',
'Enter Password': 'Entrer le mot de passe',
Password: 'Mot de passe',
Confirm: 'Confirmer'
} }
} }

View file

@ -645,6 +645,9 @@ export default {
'Create a password (or skip)': 'एक पासवर्ड बनाएं (या छोड़ें)', 'Create a password (or skip)': 'एक पासवर्ड बनाएं (या छोड़ें)',
'Enter your password again': 'अपना पासवर्ड फिर से दर्ज करें', 'Enter your password again': 'अपना पासवर्ड फिर से दर्ज करें',
'Complete Signup': 'साइनअप पूर्ण करें', 'Complete Signup': 'साइनअप पूर्ण करें',
Recommended: 'अनुशंसित' Recommended: 'अनुशंसित',
'Enter Password': 'पासवर्ड दर्ज करें',
Password: 'पासवर्ड',
Confirm: 'पुष्टि करें'
} }
} }

View file

@ -637,6 +637,9 @@ export default {
'Create a password (or skip)': 'Hozz létre jelszót (vagy hagyd ki)', 'Create a password (or skip)': 'Hozz létre jelszót (vagy hagyd ki)',
'Enter your password again': 'Add meg újra a jelszavad', 'Enter your password again': 'Add meg újra a jelszavad',
'Complete Signup': 'Regisztráció befejezése', 'Complete Signup': 'Regisztráció befejezése',
Recommended: 'Ajánlott' Recommended: 'Ajánlott',
'Enter Password': 'Jelszó megadása',
Password: 'Jelszó',
Confirm: 'Megerősítés'
} }
} }

View file

@ -649,6 +649,9 @@ export default {
'Create a password (or skip)': 'Crea una password (o salta)', 'Create a password (or skip)': 'Crea una password (o salta)',
'Enter your password again': 'Inserisci di nuovo la tua password', 'Enter your password again': 'Inserisci di nuovo la tua password',
'Complete Signup': 'Completa registrazione', 'Complete Signup': 'Completa registrazione',
Recommended: 'Consigliato' Recommended: 'Consigliato',
'Enter Password': 'Inserisci password',
Password: 'Password',
Confirm: 'Conferma'
} }
} }

View file

@ -643,6 +643,9 @@ export default {
'Create a password (or skip)': 'パスワードを作成(またはスキップ)', 'Create a password (or skip)': 'パスワードを作成(またはスキップ)',
'Enter your password again': 'パスワードをもう一度入力', 'Enter your password again': 'パスワードをもう一度入力',
'Complete Signup': '登録を完了', 'Complete Signup': '登録を完了',
Recommended: 'おすすめ' Recommended: 'おすすめ',
'Enter Password': 'パスワードを入力',
Password: 'パスワード',
Confirm: '確認'
} }
} }

View file

@ -640,6 +640,9 @@ export default {
'Create a password (or skip)': '비밀번호 생성(또는 건너뛰기)', 'Create a password (or skip)': '비밀번호 생성(또는 건너뛰기)',
'Enter your password again': '비밀번호를 다시 입력하세요', 'Enter your password again': '비밀번호를 다시 입력하세요',
'Complete Signup': '가입 완료', 'Complete Signup': '가입 완료',
Recommended: '추천' Recommended: '추천',
'Enter Password': '비밀번호 입력',
Password: '비밀번호',
Confirm: '확인'
} }
} }

View file

@ -650,6 +650,9 @@ export default {
'Create a password (or skip)': 'Utwórz hasło (lub pomiń)', 'Create a password (or skip)': 'Utwórz hasło (lub pomiń)',
'Enter your password again': 'Wprowadź hasło ponownie', 'Enter your password again': 'Wprowadź hasło ponownie',
'Complete Signup': 'Zakończ rejestrację', 'Complete Signup': 'Zakończ rejestrację',
Recommended: 'Polecane' Recommended: 'Polecane',
'Enter Password': 'Wprowadź hasło',
Password: 'Hasło',
Confirm: 'Potwierdź'
} }
} }

View file

@ -645,6 +645,9 @@ export default {
'Create a password (or skip)': 'Crie uma senha (opcional)', 'Create a password (or skip)': 'Crie uma senha (opcional)',
'Enter your password again': 'Digite sua senha novamente', 'Enter your password again': 'Digite sua senha novamente',
'Complete Signup': 'Concluir cadastro', 'Complete Signup': 'Concluir cadastro',
Recommended: 'Recomendado' Recommended: 'Recomendado',
'Enter Password': 'Digite a senha',
Password: 'Senha',
Confirm: 'Confirmar'
} }
} }

View file

@ -648,6 +648,9 @@ export default {
'Create a password (or skip)': 'Crie uma palavra-passe (ou ignore)', 'Create a password (or skip)': 'Crie uma palavra-passe (ou ignore)',
'Enter your password again': 'Introduza novamente a sua palavra-passe', 'Enter your password again': 'Introduza novamente a sua palavra-passe',
'Complete Signup': 'Concluir registo', 'Complete Signup': 'Concluir registo',
Recommended: 'Recomendado' Recommended: 'Recomendado',
'Enter Password': 'Introduza a palavra-passe',
Password: 'Palavra-passe',
Confirm: 'Confirmar'
} }
} }

View file

@ -649,6 +649,9 @@ export default {
'Create a password (or skip)': 'Создайте пароль (или пропустите)', 'Create a password (or skip)': 'Создайте пароль (или пропустите)',
'Enter your password again': 'Введите пароль еще раз', 'Enter your password again': 'Введите пароль еще раз',
'Complete Signup': 'Завершить регистрацию', 'Complete Signup': 'Завершить регистрацию',
Recommended: 'Рекомендуемые' Recommended: 'Рекомендуемые',
'Enter Password': 'Введите пароль',
Password: 'Пароль',
Confirm: 'Подтвердить'
} }
} }

View file

@ -634,6 +634,9 @@ export default {
'Create a password (or skip)': 'สร้างรหัสผ่าน (หรือข้าม)', 'Create a password (or skip)': 'สร้างรหัสผ่าน (หรือข้าม)',
'Enter your password again': 'ป้อนรหัสผ่านของคุณอีกครั้ง', 'Enter your password again': 'ป้อนรหัสผ่านของคุณอีกครั้ง',
'Complete Signup': 'เสร็จสิ้นการลงทะเบียน', 'Complete Signup': 'เสร็จสิ้นการลงทะเบียน',
Recommended: 'แนะนำ' Recommended: 'แนะนำ',
'Enter Password': 'ป้อนรหัสผ่าน',
Password: 'รหัสผ่าน',
Confirm: 'ยืนยัน'
} }
} }

View file

@ -620,6 +620,9 @@ export default {
'Create a password (or skip)': '建立密碼(或跳過)', 'Create a password (or skip)': '建立密碼(或跳過)',
'Enter your password again': '再次輸入你的密碼', 'Enter your password again': '再次輸入你的密碼',
'Complete Signup': '完成註冊', 'Complete Signup': '完成註冊',
Recommended: '推薦' Recommended: '推薦',
'Enter Password': '輸入密碼',
Password: '密碼',
Confirm: '確認'
} }
} }

View file

@ -625,6 +625,9 @@ export default {
'Create a password (or skip)': '创建密码(或跳过)', 'Create a password (or skip)': '创建密码(或跳过)',
'Enter your password again': '再次输入你的密码', 'Enter your password again': '再次输入你的密码',
'Complete Signup': '完成注册', 'Complete Signup': '完成注册',
Recommended: '推荐' Recommended: '推荐',
'Enter Password': '输入密码',
Password: '密码',
Confirm: '确认'
} }
} }

View file

@ -1,4 +1,5 @@
import LoginDialog from '@/components/LoginDialog' import LoginDialog from '@/components/LoginDialog'
import PasswordInputDialog from '@/components/PasswordInputDialog'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { import {
createDeletionRequestDraftEvent, createDeletionRequestDraftEvent,
@ -34,7 +35,7 @@ import dayjs from 'dayjs'
import { Event, kinds, VerifiedEvent } from 'nostr-tools' import { Event, kinds, VerifiedEvent } from 'nostr-tools'
import * as nip19 from 'nostr-tools/nip19' import * as nip19 from 'nostr-tools/nip19'
import * as nip49 from 'nostr-tools/nip49' import * as nip49 from 'nostr-tools/nip49'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useDeletedEvent } from '../DeletedEventProvider' import { useDeletedEvent } from '../DeletedEventProvider'
@ -128,6 +129,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [pinListEvent, setPinListEvent] = useState<Event | null>(null) const [pinListEvent, setPinListEvent] = useState<Event | null>(null)
const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1) const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
const [isInitialized, setIsInitialized] = useState(false) const [isInitialized, setIsInitialized] = useState(false)
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false)
const passwordPromiseRef = useRef<{
resolve: (password: string) => void
reject: () => void
} | null>(null)
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@ -411,6 +417,25 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
customEmojiService.init(userEmojiListEvent) customEmojiService.init(userEmojiListEvent)
}, [userEmojiListEvent]) }, [userEmojiListEvent])
const requestPassword = (): Promise<string> => {
return new Promise((resolve, reject) => {
passwordPromiseRef.current = { resolve, reject }
setPasswordDialogOpen(true)
})
}
const handlePasswordConfirm = (password: string) => {
passwordPromiseRef.current?.resolve(password)
passwordPromiseRef.current = null
setPasswordDialogOpen(false)
}
const handlePasswordCancel = () => {
passwordPromiseRef.current?.reject()
passwordPromiseRef.current = null
setPasswordDialogOpen(false)
}
const hasNostrLoginHash = () => { const hasNostrLoginHash = () => {
return window.location.hash && window.location.hash.startsWith('#nostr-login') return window.location.hash && window.location.hash.startsWith('#nostr-login')
} }
@ -485,10 +510,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
const ncryptsecLogin = async (ncryptsec: string) => { const ncryptsecLogin = async (ncryptsec: string) => {
const password = prompt(t('Enter the password to decrypt your ncryptsec')) const password = await requestPassword()
if (!password) {
throw new Error('Password is required')
}
const privkey = nip49.decrypt(ncryptsec, password) const privkey = nip49.decrypt(ncryptsec, password)
const browserNsecSigner = new NsecSigner() const browserNsecSigner = new NsecSigner()
const pubkey = browserNsecSigner.login(privkey) const pubkey = browserNsecSigner.login(privkey)
@ -567,14 +589,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
} else if (account.signerType === 'ncryptsec') { } else if (account.signerType === 'ncryptsec') {
if (account.ncryptsec) { if (account.ncryptsec) {
const password = prompt(t('Enter the password to decrypt your ncryptsec')) try {
if (!password) { const password = await requestPassword()
const privkey = nip49.decrypt(account.ncryptsec, password)
const browserNsecSigner = new NsecSigner()
browserNsecSigner.login(privkey)
return login(browserNsecSigner, account)
} catch {
return null return null
} }
const privkey = nip49.decrypt(account.ncryptsec, password)
const browserNsecSigner = new NsecSigner()
browserNsecSigner.login(privkey)
return login(browserNsecSigner, account)
} }
} else if (account.signerType === 'nip-07') { } else if (account.signerType === 'nip-07') {
const nip07Signer = new Nip07Signer() const nip07Signer = new Nip07Signer()
@ -857,6 +880,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
> >
{children} {children}
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} /> <LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
<PasswordInputDialog
open={passwordDialogOpen}
title={t('Enter Password')}
description={t('Enter the password to decrypt your ncryptsec')}
onConfirm={handlePasswordConfirm}
onCancel={handlePasswordCancel}
/>
</NostrContext.Provider> </NostrContext.Provider>
) )
} }