From a6c13413b82f37470473cb3f417cff4f4b948a72 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 17 May 2026 15:29:07 +0200 Subject: [PATCH] fix switching accounts --- src/App.tsx | 2 +- src/components/ui/sonner.tsx | 25 ++++++++++++++++++++--- src/providers/NostrProvider/index.tsx | 29 ++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e3980150..9f1ad477 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -65,7 +65,6 @@ export default function App(): JSX.Element { - @@ -83,6 +82,7 @@ export default function App(): JSX.Element { + diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index e2a0bb1d..f69bcfd6 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,4 +1,6 @@ import { useThemeOptional } from '@/providers/ThemeProvider' +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' import { Toaster as Sonner } from 'sonner' type ToasterProps = React.ComponentProps @@ -6,24 +8,41 @@ type ToasterProps = React.ComponentProps const Toaster = ({ ...props }: ToasterProps) => { const themeCtx = useThemeOptional() const themeSetting = themeCtx?.themeSetting ?? 'system' + const [mounted, setMounted] = useState(false) - return ( + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) return null + + return createPortal( + />, + document.body ) } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 9ff8b8dc..b7e55180 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -132,6 +132,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const ncryptsecPasswordResolveRef = useRef<((value: string | null) => void) | null>(null) /** One toast per mismatch episode; cleared after a successful NIP-07 login. */ const nip07KeyMismatchToastShownRef = useRef(false) + /** + * User picked a stored NIP-07 account from the notifications switcher but the extension key + * differs — we fall back to read-only npub without spamming the mismatch toast / recovery UI. + */ + const intentionalNip07ReadOnlyPubkeyRef = useRef(null) const [profile, setProfile] = useState(null) const [profileEvent, setProfileEvent] = useState(null) @@ -1074,6 +1079,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const login = (signer: ISigner, act: TAccount) => { if (act.signerType === 'nip-07') { nip07KeyMismatchToastShownRef.current = false + intentionalNip07ReadOnlyPubkeyRef.current = null } const newAccounts = storage.addAccount(act) setAccounts(newAccounts) @@ -1093,13 +1099,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } const switchAccount = async (act: TAccountPointer | null): Promise => { + intentionalNip07ReadOnlyPubkeyRef.current = null if (!act) { storage.switchAccount(null) setAccount(null) setSigner(null) return null } - const result = await loginWithAccountPointer(act) + const result = await loginWithAccountPointer(act, { userInitiatedSwitch: true }) // If loginWithAccountPointer fell back to read-only npub it skips storage.switchAccount. // Persist the user's intent here so: // • session restore on refresh targets the right account, and @@ -1228,7 +1235,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { }) } - const loginWithAccountPointer = async (act: TAccountPointer): Promise => { + const loginWithAccountPointer = async ( + act: TAccountPointer, + options?: { userInitiatedSwitch?: boolean } + ): Promise => { const fallbackToReadOnlyNpub = (pubkey: string, reason?: unknown): string => { const npubSigner = new NpubSigner() const npub = nip19.npubEncode(pubkey) @@ -1323,7 +1333,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { (isNip07SignerPubkeyMismatchError(err) || isNip07SignerPubkeyMismatchError(lastNip07Err)) && !nip07KeyMismatchToastShownRef.current ) { - fireNip07ExtensionKeyMismatchToast() + if (options?.userInitiatedSwitch) { + intentionalNip07ReadOnlyPubkeyRef.current = storedAccount.pubkey.toLowerCase() + } else { + fireNip07ExtensionKeyMismatchToast() + } } return fallbackToReadOnlyNpub(storedAccount.pubkey, err) } @@ -1366,6 +1380,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { */ const adoptCurrentExtensionNip07Identity = useEventCallback(async () => { try { + intentionalNip07ReadOnlyPubkeyRef.current = null const nip07Signer = new Nip07Signer() await nip07Signer.init() const extPubkey = await nip07Signer.getPublicKey() @@ -1450,9 +1465,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { attempts, wantedPubkey: preferred.pubkey.slice(0, 12) }) - fireNip07ExtensionKeyMismatchToast() + const quietReadOnly = + intentionalNip07ReadOnlyPubkeyRef.current === preferred.pubkey.toLowerCase() + if (!quietReadOnly) { + fireNip07ExtensionKeyMismatchToast() + } // Keep retrying — the extension may update its approved key after a moment. - schedule(3_000) + schedule(quietReadOnly ? 8_000 : 3_000) return } logger.info('[NostrProvider] NIP-07 recovery retry failed', {