Browse Source

fix switching accounts

imwald
Silberengel 4 weeks ago
parent
commit
a6c13413b8
  1. 2
      src/App.tsx
  2. 25
      src/components/ui/sonner.tsx
  3. 29
      src/providers/NostrProvider/index.tsx

2
src/App.tsx

@ -65,7 +65,6 @@ export default function App(): JSX.Element {
</LiveActivitiesProvider> </LiveActivitiesProvider>
<ReadAloudPlayerModal /> <ReadAloudPlayerModal />
<PublishSuccessSubtleIndicator /> <PublishSuccessSubtleIndicator />
<Toaster />
</UserPreferencesProvider> </UserPreferencesProvider>
</KindFilterProvider> </KindFilterProvider>
</MediaUploadServiceProvider> </MediaUploadServiceProvider>
@ -83,6 +82,7 @@ export default function App(): JSX.Element {
</div> </div>
</div> </div>
</NostrProvider> </NostrProvider>
<Toaster />
</DeletedEventProvider> </DeletedEventProvider>
</ScreenSizeProvider> </ScreenSizeProvider>
</ContentPolicyProvider> </ContentPolicyProvider>

25
src/components/ui/sonner.tsx

@ -1,4 +1,6 @@
import { useThemeOptional } from '@/providers/ThemeProvider' import { useThemeOptional } from '@/providers/ThemeProvider'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Toaster as Sonner } from 'sonner' import { Toaster as Sonner } from 'sonner'
type ToasterProps = React.ComponentProps<typeof Sonner> type ToasterProps = React.ComponentProps<typeof Sonner>
@ -6,24 +8,41 @@ type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const themeCtx = useThemeOptional() const themeCtx = useThemeOptional()
const themeSetting = themeCtx?.themeSetting ?? 'system' const themeSetting = themeCtx?.themeSetting ?? 'system'
const [mounted, setMounted] = useState(false)
return ( useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return createPortal(
<Sonner <Sonner
theme={themeSetting} theme={themeSetting}
className="toaster group" className="toaster group"
richColors richColors
mobileOffset={64} mobileOffset={64}
style={
{
'--width': '22rem',
zIndex: 9999
} as React.CSSProperties
}
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg', 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg group-[.toaster]:min-w-[min(22rem,calc(100vw-2rem))]',
description: 'group-[.toast]:text-muted-foreground', description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground', actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground' cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
},
style: {
maxWidth: 'min(420px, calc(100vw - 2rem))'
} }
}} }}
{...props} {...props}
/> />,
document.body
) )
} }

29
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) const ncryptsecPasswordResolveRef = useRef<((value: string | null) => void) | null>(null)
/** One toast per mismatch episode; cleared after a successful NIP-07 login. */ /** One toast per mismatch episode; cleared after a successful NIP-07 login. */
const nip07KeyMismatchToastShownRef = useRef(false) 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<string | null>(null)
const [profile, setProfile] = useState<TProfile | null>(null) const [profile, setProfile] = useState<TProfile | null>(null)
const [profileEvent, setProfileEvent] = useState<Event | null>(null) const [profileEvent, setProfileEvent] = useState<Event | null>(null)
@ -1074,6 +1079,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const login = (signer: ISigner, act: TAccount) => { const login = (signer: ISigner, act: TAccount) => {
if (act.signerType === 'nip-07') { if (act.signerType === 'nip-07') {
nip07KeyMismatchToastShownRef.current = false nip07KeyMismatchToastShownRef.current = false
intentionalNip07ReadOnlyPubkeyRef.current = null
} }
const newAccounts = storage.addAccount(act) const newAccounts = storage.addAccount(act)
setAccounts(newAccounts) setAccounts(newAccounts)
@ -1093,13 +1099,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
const switchAccount = async (act: TAccountPointer | null): Promise<string | null> => { const switchAccount = async (act: TAccountPointer | null): Promise<string | null> => {
intentionalNip07ReadOnlyPubkeyRef.current = null
if (!act) { if (!act) {
storage.switchAccount(null) storage.switchAccount(null)
setAccount(null) setAccount(null)
setSigner(null) setSigner(null)
return 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. // If loginWithAccountPointer fell back to read-only npub it skips storage.switchAccount.
// Persist the user's intent here so: // Persist the user's intent here so:
// • session restore on refresh targets the right account, and // • 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<string | null> => { const loginWithAccountPointer = async (
act: TAccountPointer,
options?: { userInitiatedSwitch?: boolean }
): Promise<string | null> => {
const fallbackToReadOnlyNpub = (pubkey: string, reason?: unknown): string => { const fallbackToReadOnlyNpub = (pubkey: string, reason?: unknown): string => {
const npubSigner = new NpubSigner() const npubSigner = new NpubSigner()
const npub = nip19.npubEncode(pubkey) const npub = nip19.npubEncode(pubkey)
@ -1323,7 +1333,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
(isNip07SignerPubkeyMismatchError(err) || isNip07SignerPubkeyMismatchError(lastNip07Err)) && (isNip07SignerPubkeyMismatchError(err) || isNip07SignerPubkeyMismatchError(lastNip07Err)) &&
!nip07KeyMismatchToastShownRef.current !nip07KeyMismatchToastShownRef.current
) { ) {
fireNip07ExtensionKeyMismatchToast() if (options?.userInitiatedSwitch) {
intentionalNip07ReadOnlyPubkeyRef.current = storedAccount.pubkey.toLowerCase()
} else {
fireNip07ExtensionKeyMismatchToast()
}
} }
return fallbackToReadOnlyNpub(storedAccount.pubkey, err) return fallbackToReadOnlyNpub(storedAccount.pubkey, err)
} }
@ -1366,6 +1380,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
*/ */
const adoptCurrentExtensionNip07Identity = useEventCallback(async () => { const adoptCurrentExtensionNip07Identity = useEventCallback(async () => {
try { try {
intentionalNip07ReadOnlyPubkeyRef.current = null
const nip07Signer = new Nip07Signer() const nip07Signer = new Nip07Signer()
await nip07Signer.init() await nip07Signer.init()
const extPubkey = await nip07Signer.getPublicKey() const extPubkey = await nip07Signer.getPublicKey()
@ -1450,9 +1465,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
attempts, attempts,
wantedPubkey: preferred.pubkey.slice(0, 12) 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. // Keep retrying — the extension may update its approved key after a moment.
schedule(3_000) schedule(quietReadOnly ? 8_000 : 3_000)
return return
} }
logger.info('[NostrProvider] NIP-07 recovery retry failed', { logger.info('[NostrProvider] NIP-07 recovery retry failed', {

Loading…
Cancel
Save