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', {