Browse Source

fix key switching with extension

anon signing
imwald
Silberengel 2 weeks ago
parent
commit
cf2611b63c
  1. 4
      src/components/PostEditor/PostContent.tsx
  2. 15
      src/components/PostEditor/index.tsx
  3. 276
      src/components/StoredAccountSwitchSelect.tsx
  4. 7
      src/i18n/locales/de.ts
  5. 7
      src/i18n/locales/en.ts
  6. 4
      src/pages/primary/SpellsPage/index.tsx
  7. 33
      src/providers/NostrProvider/index.tsx
  8. 4
      src/providers/nostr-context.tsx

4
src/components/PostEditor/PostContent.tsx

@ -1,4 +1,5 @@
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import Note from '@/components/Note' import Note from '@/components/Note'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
@ -3520,6 +3521,9 @@ export default function PostContent({
{parentEvent ? t('Reply') : isDiscussionThread ? t('Create Thread') : t('Post')} {parentEvent ? t('Reply') : isDiscussionThread ? t('Create Thread') : t('Post')}
</Button> </Button>
</div> </div>
{open ? (
<StoredAccountSwitchSelect withTopBorder alignEnd className="w-full" showLabelAlways />
) : null}
{/* Media Kind Selection Dialog */} {/* Media Kind Selection Dialog */}
<Dialog open={showMediaKindDialog} onOpenChange={setShowMediaKindDialog}> <Dialog open={showMediaKindDialog} onOpenChange={setShowMediaKindDialog}>

15
src/components/PostEditor/index.tsx

@ -1,4 +1,3 @@
import StoredAccountSwitchSelect from '@/components/StoredAccountSwitchSelect'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -103,13 +102,6 @@ export default function PostEditor({
<SheetTitle>Post Editor</SheetTitle> <SheetTitle>Post Editor</SheetTitle>
<SheetDescription>Create a new post or reply</SheetDescription> <SheetDescription>Create a new post or reply</SheetDescription>
</SheetHeader> </SheetHeader>
{open ? (
<StoredAccountSwitchSelect
withBottomBorder
className="w-full flex-wrap"
showLabelAlways
/>
) : null}
{content} {content}
</div> </div>
</ScrollArea> </ScrollArea>
@ -136,13 +128,6 @@ export default function PostEditor({
<DialogTitle>Post Editor</DialogTitle> <DialogTitle>Post Editor</DialogTitle>
<DialogDescription>Create a new post or reply</DialogDescription> <DialogDescription>Create a new post or reply</DialogDescription>
</DialogHeader> </DialogHeader>
{open ? (
<StoredAccountSwitchSelect
withBottomBorder
className="w-full flex-wrap"
showLabelAlways
/>
) : null}
{content} {content}
</div> </div>
</ScrollArea> </ScrollArea>

276
src/components/StoredAccountSwitchSelect.tsx

@ -1,8 +1,12 @@
import UserAvatar from '@/components/UserAvatar' import { SimpleUserAvatar } from '@/components/UserAvatar'
import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button'
import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { Nip07Signer } from '@/providers/NostrProvider/nip-07.signer'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useCallback, useMemo } from 'react' import type { TAccountPointer } from '@/types'
import { Loader2 } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -11,73 +15,182 @@ type Props = {
triggerClassName?: string triggerClassName?: string
/** Show the inline label on narrow viewports (e.g. full-screen post composer). */ /** Show the inline label on narrow viewports (e.g. full-screen post composer). */
showLabelAlways?: boolean showLabelAlways?: boolean
/** Separator under the row (e.g. post editor header). */ /** Separator under the row. */
withBottomBorder?: boolean withBottomBorder?: boolean
/** Separator above the row (e.g. post editor footer). */
withTopBorder?: boolean
/** Align chips to the end (e.g. beside the publish button). */
alignEnd?: boolean
}
function dedupeStoredAccounts(accounts: TAccountPointer[]): TAccountPointer[] {
const seen = new Set<string>()
const out: TAccountPointer[] = []
for (const a of accounts) {
const raw = a.pubkey?.trim()
if (!raw) continue
const p = normalizeHexPubkey(raw)
if (seen.has(p)) continue
seen.add(p)
out.push(a)
}
return out
} }
/** /**
* Switch {@link useNostr} session among stored accounts (same as notifications spell). * Switch {@link useNostr} session among stored accounts (notifications spell, post editor).
* Renders nothing when there is only one stored account or no session. * Avatar chips instead of a native select; NIP-07 extension sync hint + retry when read-only.
*
* Uses a native {@link HTMLSelectElement} instead of Radix Select: nested `UserAvatar` /
* `Username` inside `SelectItem` composes refs in ways that have triggered
* Maximum update depth exceeded on this page (Radix `compose-refs` + frequent re-renders).
*/ */
export default function StoredAccountSwitchSelect({ export default function StoredAccountSwitchSelect({
className, className,
triggerClassName,
showLabelAlways = false, showLabelAlways = false,
withBottomBorder = false withBottomBorder = false,
withTopBorder = false,
alignEnd = false
}: Props) { }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, accounts, switchAccount, isAccountSessionHydrating } = useNostr() const {
pubkey,
account,
accounts,
switchAccount,
isAccountSessionHydrating,
retryNip07SignerForPreferredAccount,
adoptExtensionNip07Identity
} = useNostr()
const [switchingPubkey, setSwitchingPubkey] = useState<string | null>(null)
const [retryingExtension, setRetryingExtension] = useState(false)
const [extensionPubkey, setExtensionPubkey] = useState<string | null>(null)
const sessionPubkey = useMemo(() => { const sessionPubkey = useMemo(() => {
const cur = pubkey?.trim() const cur = pubkey?.trim()
return cur ? normalizeHexPubkey(cur) : null return cur ? normalizeHexPubkey(cur) : null
}, [pubkey]) }, [pubkey])
const storedAccountPubkeys = useMemo(() => { const storedAccounts = useMemo(() => dedupeStoredAccounts(accounts), [accounts])
const seen = new Set<string>()
const out: string[] = [] const activeStoredAccount = useMemo(() => {
for (const a of accounts) { if (!sessionPubkey) return null
const raw = a.pubkey?.trim() return (
if (!raw) continue storedAccounts.find((a) => hexPubkeysEqual(normalizeHexPubkey(a.pubkey), sessionPubkey)) ?? null
const p = normalizeHexPubkey(raw) )
if (!seen.has(p)) { }, [storedAccounts, sessionPubkey])
seen.add(p)
out.push(p) const needsExtensionSync = useMemo(() => {
if (!activeStoredAccount || !account) return false
return activeStoredAccount.signerType === 'nip-07' && account.signerType === 'npub'
}, [activeStoredAccount, account])
const extensionDiffersFromSession = useMemo(() => {
if (!extensionPubkey || !sessionPubkey) return false
return !hexPubkeysEqual(normalizeHexPubkey(extensionPubkey), sessionPubkey)
}, [extensionPubkey, sessionPubkey])
useEffect(() => {
if (!needsExtensionSync) {
setExtensionPubkey(null)
return
} }
let cancelled = false
const poll = async () => {
try {
const nip07Signer = new Nip07Signer()
await nip07Signer.init()
const pk = await nip07Signer.getPublicKey()
if (cancelled || !pk?.trim()) return
setExtensionPubkey(pk)
if (
sessionPubkey &&
hexPubkeysEqual(normalizeHexPubkey(pk), sessionPubkey) &&
!retryingExtension
) {
const ok = await retryNip07SignerForPreferredAccount()
if (!cancelled && ok) {
toast.success(t('accountSwitch.extensionConnected'))
} }
return out }
}, [accounts]) } catch {
if (!cancelled) setExtensionPubkey(null)
}
}
void poll()
const id = window.setInterval(() => void poll(), 2_000)
return () => {
cancelled = true
window.clearInterval(id)
}
}, [
needsExtensionSync,
sessionPubkey,
retryNip07SignerForPreferredAccount,
retryingExtension,
t
])
const handlePick = useCallback( const handlePick = useCallback(
async (v: string) => { async (nextAccount: TAccountPointer) => {
const target = normalizeHexPubkey(v) const target = normalizeHexPubkey(nextAccount.pubkey)
if (pubkey && hexPubkeysEqual(target, normalizeHexPubkey(pubkey))) return if (sessionPubkey && hexPubkeysEqual(target, sessionPubkey)) return
const nextAccount = accounts.find((a) => hexPubkeysEqual(normalizeHexPubkey(a.pubkey), target)) setSwitchingPubkey(target)
if (!nextAccount) { try {
const switched = await switchAccount(nextAccount)
if (!switched) {
toast.error(t('notificationsSwitchAccountFailed')) toast.error(t('notificationsSwitchAccountFailed'))
return return
} }
const switched = await switchAccount(nextAccount) if (!hexPubkeysEqual(normalizeHexPubkey(switched), target)) {
if (!switched || !hexPubkeysEqual(normalizeHexPubkey(switched), target)) {
toast.error(t('notificationsSwitchAccountFailed')) toast.error(t('notificationsSwitchAccountFailed'))
return
}
if (nextAccount.signerType === 'nip-07') {
await retryNip07SignerForPreferredAccount()
}
} finally {
setSwitchingPubkey(null)
} }
}, },
[pubkey, accounts, switchAccount, t] [sessionPubkey, switchAccount, retryNip07SignerForPreferredAccount, t]
) )
if (storedAccountPubkeys.length <= 1 || !sessionPubkey) return null const handleRetryExtension = useCallback(async () => {
setRetryingExtension(true)
try {
const ok = await retryNip07SignerForPreferredAccount()
if (ok) {
toast.success(t('accountSwitch.extensionConnected'))
} else {
toast.error(t('accountSwitch.extensionRetryFailed'))
}
} finally {
setRetryingExtension(false)
}
}, [retryNip07SignerForPreferredAccount, t])
if (storedAccounts.length <= 1 || !sessionPubkey) return null
const busy = isAccountSessionHydrating || switchingPubkey !== null
return ( return (
<div <div
className={cn( className={cn(
'flex min-w-0 items-center gap-2', 'flex min-w-0 flex-col gap-2',
alignEnd && 'items-end',
withBottomBorder && '-mx-1 mb-1 border-b border-border/60 px-1 pb-3', withBottomBorder && '-mx-1 mb-1 border-b border-border/60 px-1 pb-3',
withTopBorder &&
(alignEnd
? '-mx-1 mt-1 border-t border-border/60 px-1 pt-2'
: '-mx-1 mt-2 border-t border-border/60 px-1 pt-3'),
className className
)} )}
role="group"
aria-label={t('notificationsViewAsAccountAria')}
>
<div
className={cn(
'flex min-w-0 flex-wrap items-center gap-2',
alignEnd && 'justify-end'
)}
> >
<span <span
className={cn( className={cn(
@ -87,23 +200,88 @@ export default function StoredAccountSwitchSelect({
> >
{t('notificationsViewAsAccount')} {t('notificationsViewAsAccount')}
</span> </span>
<UserAvatar userId={sessionPubkey} size="small" className="shrink-0" /> <div className="flex min-w-0 flex-wrap items-center gap-1.5">
<select {storedAccounts.map((act) => {
const pk = normalizeHexPubkey(act.pubkey)
const isActive = hexPubkeysEqual(pk, sessionPubkey)
const isSwitching = switchingPubkey !== null && hexPubkeysEqual(pk, switchingPubkey)
const readOnlyChip =
isActive && act.signerType === 'nip-07' && account?.signerType === 'npub'
return (
<button
key={`${pk}-${act.signerType}`}
type="button"
disabled={busy && !isSwitching}
aria-pressed={isActive}
aria-label={t('accountSwitch.selectAccount', {
pubkey: formatPubkey(pk),
defaultValue: `Switch to ${formatPubkey(pk)}`
})}
title={t('accountSwitch.selectAccount', {
pubkey: formatPubkey(pk),
defaultValue: `Switch to ${formatPubkey(pk)}`
})}
className={cn( className={cn(
'h-9 min-w-0 flex-1 cursor-pointer rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50', 'relative shrink-0 rounded-full p-0.5 transition-[box-shadow,opacity]',
triggerClassName 'ring-2 ring-offset-2 ring-offset-background',
isActive
? readOnlyChip
? 'ring-amber-500/90'
: 'ring-primary'
: 'ring-transparent hover:ring-muted-foreground/35',
busy && !isSwitching && 'opacity-50'
)} )}
value={sessionPubkey} onClick={() => void handlePick(act)}
disabled={isAccountSessionHydrating}
aria-label={t('notificationsViewAsAccountAria')}
onChange={(e) => void handlePick(e.target.value)}
> >
{storedAccountPubkeys.map((pk) => ( <SimpleUserAvatar userId={pk} size="small" deferRemoteAvatar={false} />
<option key={pk} value={pk}> {isSwitching ? (
{formatPubkey(pk)} <span className="absolute inset-0 flex items-center justify-center rounded-full bg-background/70">
</option> <Loader2 className="size-4 animate-spin text-muted-foreground" aria-hidden />
))} </span>
</select> ) : null}
</button>
)
})}
</div>
</div>
{needsExtensionSync ? (
<div
className={cn(
'rounded-md border border-amber-500/35 bg-amber-500/10 px-2.5 py-2 text-xs text-amber-950 dark:text-amber-100',
alignEnd && 'max-w-md self-end'
)}
>
<p className="leading-relaxed">{t('accountSwitch.extensionSyncHint')}</p>
<div className="mt-2 flex flex-wrap gap-2">
<Button
type="button"
size="sm"
variant="secondary"
className="h-7 text-xs"
disabled={retryingExtension || busy}
onClick={() => void handleRetryExtension()}
>
{retryingExtension ? (
<Loader2 className="mr-1 size-3.5 animate-spin" aria-hidden />
) : null}
{t('accountSwitch.extensionRetry')}
</Button>
{extensionDiffersFromSession ? (
<Button
type="button"
size="sm"
variant="outline"
className="h-7 text-xs"
disabled={retryingExtension || busy}
onClick={() => void adoptExtensionNip07Identity()}
>
{t('nip07.useExtensionIdentity')}
</Button>
) : null}
</div>
</div>
) : null}
</div> </div>
) )
} }

7
src/i18n/locales/de.ts

@ -995,6 +995,13 @@ export default {
'Gespeichertes Konto wechseln. Benachrichtigungen, Antworten, Zaps, Reaktionen und Boosts nutzen dieses Konto.', 'Gespeichertes Konto wechseln. Benachrichtigungen, Antworten, Zaps, Reaktionen und Boosts nutzen dieses Konto.',
notificationsSwitchAccountFailed: notificationsSwitchAccountFailed:
'Kontowechsel fehlgeschlagen. Signierer, Passwort oder Erweiterung prüfen und erneut versuchen.', 'Kontowechsel fehlgeschlagen. Signierer, Passwort oder Erweiterung prüfen und erneut versuchen.',
'accountSwitch.selectAccount': 'Zu {{pubkey}} wechseln',
'accountSwitch.extensionSyncHint':
'Dieses Konto nutzt eine Browser-Erweiterung. Wähle denselben Schlüssel in der Erweiterung (Alby, nos2x, …) und tippe auf „Erneut“ — die App verbindet sich automatisch, sobald die Schlüssel übereinstimmen.',
'accountSwitch.extensionRetry': 'Erweiterung erneut',
'accountSwitch.extensionConnected': 'Erweiterung für dieses Konto verbunden.',
'accountSwitch.extensionRetryFailed':
'Erweiterungsschlüssel passt noch nicht. Schlüssel in der Erweiterung wechseln und erneut versuchen.',
'Show untrusted {type}': 'Untrusted {{type}} anzeigen', 'Show untrusted {type}': 'Untrusted {{type}} anzeigen',
'Hide untrusted {type}': 'Untrusted {{type}} ausblenden', 'Hide untrusted {type}': 'Untrusted {{type}} ausblenden',
'Currently hiding {type} from untrusted users.': 'Currently hiding {type} from untrusted users.':

7
src/i18n/locales/en.ts

@ -1012,6 +1012,13 @@ export default {
'Switch stored account. Notifications, replies, zaps, reactions, and boosts use this account.', 'Switch stored account. Notifications, replies, zaps, reactions, and boosts use this account.',
notificationsSwitchAccountFailed: notificationsSwitchAccountFailed:
'Could not switch account. Check your signer, password, or extension and try again.', 'Could not switch account. Check your signer, password, or extension and try again.',
'accountSwitch.selectAccount': 'Switch to {{pubkey}}',
'accountSwitch.extensionSyncHint':
'This account uses a browser extension. Select the same key in your extension (Alby, nos2x, …), then tap Retry — the app will connect automatically when the keys match.',
'accountSwitch.extensionRetry': 'Retry extension',
'accountSwitch.extensionConnected': 'Extension connected for this account.',
'accountSwitch.extensionRetryFailed':
'Extension key still does not match. Switch the key in your extension, then try again.',
'Show untrusted {type}': 'Show untrusted {{type}}', 'Show untrusted {type}': 'Show untrusted {{type}}',
'Hide untrusted {type}': 'Hide untrusted {{type}}', 'Hide untrusted {type}': 'Hide untrusted {{type}}',
'Currently hiding {type} from untrusted users.': 'Currently hiding {type} from untrusted users.':

4
src/pages/primary/SpellsPage/index.tsx

@ -1062,9 +1062,9 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
) : selectedFauxSpell && fauxSubRequests.length > 0 ? ( ) : selectedFauxSpell && fauxSubRequests.length > 0 ? (
<> <>
{selectedFauxSpell === 'notifications' ? ( {selectedFauxSpell === 'notifications' ? (
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2 px-1 pb-2 sm:justify-between"> <div className="sticky top-0 z-10 shrink-0 border-b border-border/50 bg-background/95 px-1 py-2 backdrop-blur-sm supports-[backdrop-filter]:bg-background/80">
{notificationsFeedPubkey ? ( {notificationsFeedPubkey ? (
<StoredAccountSwitchSelect className="min-w-0 flex-1 sm:max-w-[min(100%,20rem)]" /> <StoredAccountSwitchSelect className="w-full" showLabelAlways />
) : null} ) : null}
</div> </div>
) : null} ) : null}

33
src/providers/NostrProvider/index.tsx

@ -1471,6 +1471,31 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
* Stored NIP-07 account pubkey no longer matches the extension (user switched keys). * Stored NIP-07 account pubkey no longer matches the extension (user switched keys).
* Drop the stale stored NIP-07 row and sign in with whatever pubkey the extension returns now. * Drop the stale stored NIP-07 row and sign in with whatever pubkey the extension returns now.
*/ */
const retryNip07SignerForPreferredAccount = useEventCallback(async (): Promise<boolean> => {
const preferred = storage.getCurrentAccount()
if (!preferred || preferred.signerType !== 'nip-07') return false
try {
const nip07Signer = new Nip07Signer()
await nip07Signer.init()
const extPubkey = await nip07Signer.getPublicKey()
if (!extPubkey?.trim()) return false
if (extPubkey.toLowerCase() !== preferred.pubkey.toLowerCase()) {
return false
}
intentionalNip07ReadOnlyPubkeyRef.current = null
nip07KeyMismatchToastShownRef.current = false
login(nip07Signer, preferred)
setNip07RecoveryBump((b) => b + 1)
return true
} catch (e) {
logger.info('[NostrProvider] NIP-07 retry for preferred account failed', {
pubkeySlice: preferred.pubkey.slice(0, 12),
error: e instanceof Error ? e.message : String(e)
})
return false
}
})
const adoptCurrentExtensionNip07Identity = useEventCallback(async () => { const adoptCurrentExtensionNip07Identity = useEventCallback(async () => {
try { try {
intentionalNip07ReadOnlyPubkeyRef.current = null intentionalNip07ReadOnlyPubkeyRef.current = null
@ -1560,7 +1585,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
fireNip07ExtensionKeyMismatchToast() fireNip07ExtensionKeyMismatchToast()
} }
// Keep retrying — the extension may update its approved key after a moment. // Keep retrying — the extension may update its approved key after a moment.
schedule(quietReadOnly ? 8_000 : 3_000) schedule(quietReadOnly ? 2_000 : 3_000)
return return
} }
logger.info('[NostrProvider] NIP-07 recovery retry failed', { logger.info('[NostrProvider] NIP-07 recovery retry failed', {
@ -2064,6 +2089,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const removeAccountStable = useEventCallback(removeAccount) const removeAccountStable = useEventCallback(removeAccount)
const discardLocalPrivateKeyStable = useEventCallback(discardLocalPrivateKey) const discardLocalPrivateKeyStable = useEventCallback(discardLocalPrivateKey)
const switchAccountStable = useEventCallback(switchAccount) const switchAccountStable = useEventCallback(switchAccount)
const retryNip07SignerForPreferredAccountStable = useEventCallback(retryNip07SignerForPreferredAccount)
const adoptExtensionNip07IdentityStable = useEventCallback(adoptCurrentExtensionNip07Identity)
const nsecLoginStable = useEventCallback(nsecLogin) const nsecLoginStable = useEventCallback(nsecLogin)
const ncryptsecLoginStable = useEventCallback(ncryptsecLogin) const ncryptsecLoginStable = useEventCallback(ncryptsecLogin)
const npubLoginStable = useEventCallback(npubLogin) const npubLoginStable = useEventCallback(npubLogin)
@ -2113,6 +2140,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
nsec, nsec,
ncryptsec, ncryptsec,
switchAccount: switchAccountStable, switchAccount: switchAccountStable,
retryNip07SignerForPreferredAccount: retryNip07SignerForPreferredAccountStable,
adoptExtensionNip07Identity: adoptExtensionNip07IdentityStable,
nsecLogin: nsecLoginStable, nsecLogin: nsecLoginStable,
ncryptsecLogin: ncryptsecLoginStable, ncryptsecLogin: ncryptsecLoginStable,
nip07Login: nip07LoginStable, nip07Login: nip07LoginStable,
@ -2180,6 +2209,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
signHttpAuthStable, signHttpAuthStable,
startLogin, startLogin,
switchAccountStable, switchAccountStable,
retryNip07SignerForPreferredAccountStable,
adoptExtensionNip07IdentityStable,
updateBlockedRelaysEventStable, updateBlockedRelaysEventStable,
updateBookmarkListEventStable, updateBookmarkListEventStable,
updateCacheRelayListEventStable, updateCacheRelayListEventStable,

4
src/providers/nostr-context.tsx

@ -37,6 +37,10 @@ export type TNostrContext = {
ncryptsec: string | null ncryptsec: string | null
/** Returns the new session pubkey on success, or `null` if logout / switch failed. */ /** Returns the new session pubkey on success, or `null` if logout / switch failed. */
switchAccount: (account: TAccountPointer | null) => Promise<string | null> switchAccount: (account: TAccountPointer | null) => Promise<string | null>
/** Reconnect NIP-07 when the extension pubkey matches the stored preferred account. */
retryNip07SignerForPreferredAccount: () => Promise<boolean>
/** Sign in with whichever pubkey the browser extension exposes now. */
adoptExtensionNip07Identity: () => Promise<void>
nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise<string> nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise<string>
ncryptsecLogin: (ncryptsec: string) => Promise<string> ncryptsecLogin: (ncryptsec: string) => Promise<string>
nip07Login: () => Promise<string> nip07Login: () => Promise<string>

Loading…
Cancel
Save