Browse Source

fix nsec login and etc. for Electron

imwald
Silberengel 1 month ago
parent
commit
474eb5420a
  1. 98
      src/components/NcryptsecPasswordPrompt/index.tsx
  2. 133
      src/components/SaveRelayDropdownMenu/index.tsx
  3. 41
      src/providers/NostrProvider/index.tsx

98
src/components/NcryptsecPasswordPrompt/index.tsx

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
open: boolean
onResult: (password: string | null) => void
}
export default function NcryptsecPasswordPrompt({ open, onResult }: Props) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [password, setPassword] = useState('')
useEffect(() => {
if (open) setPassword('')
}, [open])
const title = t('Enter the password to decrypt your ncryptsec')
const submit = () => {
const trimmed = password.trim()
onResult(trimmed.length > 0 ? trimmed : null)
}
const cancel = () => {
onResult(null)
}
const form = (
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault()
submit()
}}
>
<div className="grid gap-2">
<Label htmlFor="ncryptsec-unlock-password">{t('password')}</Label>
<Input
id="ncryptsec-unlock-password"
type="password"
autoComplete="current-password"
autoFocus
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="secondary" onClick={cancel}>
{t('Cancel')}
</Button>
<Button type="submit">{t('Continue')}</Button>
</div>
</form>
)
if (isSmallScreen) {
return (
<Drawer
open={open}
onOpenChange={(next) => {
if (!next) cancel()
}}
>
<DrawerContent className="max-h-[90vh]">
<DrawerHeader>
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription className="sr-only">{title}</DrawerDescription>
</DrawerHeader>
<div className="p-4 pt-0">{form}</div>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next) cancel()
}}
>
<DialogContent className="w-[400px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">{title}</DialogDescription>
</DialogHeader>
{form}
</DialogContent>
</Dialog>
)
}

133
src/components/SaveRelayDropdownMenu/index.tsx

@ -1,7 +1,15 @@ @@ -1,7 +1,15 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerOverlay,
DrawerTitle
@ -14,6 +22,8 @@ import { @@ -14,6 +22,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton'
import { normalizeUrl } from '@/lib/url'
@ -22,7 +32,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -22,7 +32,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { TRelaySet } from '@/types'
import { Ban, Check, FolderPlus, Plus, Star } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import DrawerMenuItem from '../DrawerMenuItem'
import logger from '@/lib/logger'
@ -207,32 +217,133 @@ function SaveToNewSet({ urls }: { urls: string[] }) { @@ -207,32 +217,133 @@ function SaveToNewSet({ urls }: { urls: string[] }) {
const { isSmallScreen } = useScreenSize()
const { pubkey, startLogin } = useNostr()
const { createRelaySet } = useFavoriteRelays()
const [namePromptOpen, setNamePromptOpen] = useState(false)
const handleSave = () => {
const openNamePrompt = () => {
if (!pubkey) {
startLogin()
return
}
const newSetName = prompt(t('Enter a name for the new relay set'))
if (newSetName) {
createRelaySet(newSetName, urls)
setNamePromptOpen(true)
}
const onNameResult = (name: string | null) => {
setNamePromptOpen(false)
if (name) {
createRelaySet(name, urls)
}
}
if (isSmallScreen) {
return (
<DrawerMenuItem onClick={handleSave}>
<>
{isSmallScreen ? (
<DrawerMenuItem onClick={openNamePrompt}>
<FolderPlus />
{t('Save to a new relay set')}
</DrawerMenuItem>
) : (
<DropdownMenuItem onClick={openNamePrompt}>
<FolderPlus />
{t('Save to a new relay set')}
</DropdownMenuItem>
)}
<RelaySetNamePrompt
open={namePromptOpen}
title={t('Enter a name for the new relay set')}
onResult={onNameResult}
/>
</>
)
}
function RelaySetNamePrompt({
open,
title,
onResult
}: {
open: boolean
title: string
onResult: (name: string | null) => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [value, setValue] = useState('')
useEffect(() => {
if (open) setValue('')
}, [open])
const submit = () => {
const trimmed = value.trim()
onResult(trimmed.length > 0 ? trimmed : null)
}
const cancel = () => {
onResult(null)
}
const form = (
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault()
submit()
}}
>
<div className="grid gap-2">
<Label htmlFor="relay-set-name-input">{t('Name')}</Label>
<Input
id="relay-set-name-input"
type="text"
autoComplete="off"
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="secondary" onClick={cancel}>
{t('Cancel')}
</Button>
<Button type="submit">{t('Save')}</Button>
</div>
</form>
)
if (isSmallScreen) {
return (
<DropdownMenuItem onClick={handleSave}>
<FolderPlus />
{t('Save to a new relay set')}
</DropdownMenuItem>
<Drawer
open={open}
onOpenChange={(next) => {
if (!next) cancel()
}}
>
<DrawerContent className="max-h-[90vh]">
<DrawerHeader>
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription className="sr-only">{title}</DrawerDescription>
</DrawerHeader>
<div className="p-4 pt-0">{form}</div>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next) cancel()
}}
>
<DialogContent className="w-[400px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">{title}</DialogDescription>
</DialogHeader>
{form}
</DialogContent>
</Dialog>
)
}

41
src/providers/NostrProvider/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import LoginDialog from '@/components/LoginDialog'
import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt'
import {
ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS,
DEFAULT_FAVORITE_RELAYS,
@ -95,6 +96,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -95,6 +96,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [ncryptsec, setNcryptsec] = useState<string | null>(null)
const [signer, setSigner] = useState<ISigner | null>(null)
const [openLoginDialog, setOpenLoginDialog] = useState(false)
const [ncryptsecPasswordOpen, setNcryptsecPasswordOpen] = useState(false)
const ncryptsecPasswordResolveRef = useRef<((value: string | null) => void) | null>(null)
const [profile, setProfile] = useState<TProfile | null>(null)
// Cleanup on page unload to prevent extension UI issues
@ -812,6 +815,23 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -812,6 +815,23 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await loginWithAccountPointer(act)
}
const finishNcryptsecPasswordPrompt = useCallback((password: string | null) => {
const resolve = ncryptsecPasswordResolveRef.current
if (!resolve) return
ncryptsecPasswordResolveRef.current = null
setNcryptsecPasswordOpen(false)
resolve(password)
}, [])
const askNcryptsecPassword = useCallback((): Promise<string | null> => {
return new Promise((resolve) => {
const prev = ncryptsecPasswordResolveRef.current
if (prev) prev(null)
ncryptsecPasswordResolveRef.current = resolve
setNcryptsecPasswordOpen(true)
})
}, [])
const nsecLogin = async (nsecOrHex: string, password?: string, needSetup?: boolean) => {
const nsecSigner = new NsecSigner()
let privkey: Uint8Array
@ -840,11 +860,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -840,11 +860,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
const ncryptsecLogin = async (ncryptsec: string) => {
const password = prompt(t('Enter the password to decrypt your ncryptsec'))
const password = await askNcryptsecPassword()
if (!password) {
throw new Error('Password is required')
}
const privkey = nip49.decrypt(ncryptsec, password)
let privkey: Uint8Array
try {
privkey = nip49.decrypt(ncryptsec, password)
} catch (e) {
toast.error(t('Login failed') + ': ' + (e as Error).message)
throw e
}
const browserNsecSigner = new NsecSigner()
const pubkey = browserNsecSigner.login(privkey)
return login(browserNsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
@ -922,11 +948,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -922,11 +948,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
} else if (account.signerType === 'ncryptsec') {
if (account.ncryptsec) {
const password = prompt(t('Enter the password to decrypt your ncryptsec'))
const password = await askNcryptsecPassword()
if (!password) {
return null
}
const privkey = nip49.decrypt(account.ncryptsec, password)
let privkey: Uint8Array
try {
privkey = nip49.decrypt(account.ncryptsec, password)
} catch (e) {
toast.error(t('Login failed') + ': ' + (e as Error).message)
return null
}
const browserNsecSigner = new NsecSigner()
browserNsecSigner.login(privkey)
return login(browserNsecSigner, account)
@ -1376,6 +1408,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1376,6 +1408,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
>
{children}
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
<NcryptsecPasswordPrompt open={ncryptsecPasswordOpen} onResult={finishNcryptsecPasswordPrompt} />
</NostrContext.Provider>
)
}

Loading…
Cancel
Save