fix(amber): stop re-decrypting on every navigation
Root cause: Layout is NOT a router layout — ~62 pages wrap themselves in <Layout>, so its chrome remounts on every route change. The header's NotificationBell -> useNotificationsRead -> useReadState -> useSyncedAppData re-decrypts an encrypted kind-30078 event on mount; with Amber (NIP-55) every nip44.decrypt opens the signer app, so it popped on each menu click. Fix: for interactive signers, decrypt once per session and serve remounts from cache (staleTime/gcTime Infinity, refetchOnMount/refetchOnReconnect false) in useSyncedAppData and useStash. Non-interactive signers refetch as before. Follow-up (noted in AGENTS.md): hoist Layout into the router so the chrome mounts once — removes the whole remount class (and SasquatchGuide state resets).
This commit is contained in:
parent
1e4f3302dc
commit
55b4dd2e86
3 changed files with 27 additions and 2 deletions
|
|
@ -150,6 +150,13 @@ session with **no other memory of this project**. So:
|
||||||
MOTIS 2, open CORS) — note origin/destination go to that service, unlike road
|
MOTIS 2, open CORS) — note origin/destination go to that service, unlike road
|
||||||
modes. **No external map-app handoff anywhere** (product decision) and no
|
modes. **No external map-app handoff anywhere** (product decision) and no
|
||||||
scooter/moped profile.
|
scooter/moped profile.
|
||||||
|
- **⚠️ `Layout` is NOT a router layout** — ~62 pages wrap themselves in
|
||||||
|
`<Layout>`, so its chrome (`NotificationBell`, `SasquatchGuide`, nav) **remounts
|
||||||
|
on every navigation**. Consequence: any *decrypt-on-mount* hook in that chrome
|
||||||
|
re-opens Amber (NIP-55) on every page change. Encrypted hooks must be gated for
|
||||||
|
interactive signers: `staleTime/gcTime: Infinity`, `refetchOnMount: false`,
|
||||||
|
`refetchOnReconnect: false` (see `useSyncedAppData` / `useStash`). Hoisting
|
||||||
|
`Layout` into the router would fix the whole class — a known follow-up.
|
||||||
- **Mints directory**: `/mints` page lists Cashu mints from the community
|
- **Mints directory**: `/mints` page lists Cashu mints from the community
|
||||||
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
|
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
|
||||||
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
|
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,12 @@ export function useStash(options?: { enabled?: boolean }) {
|
||||||
},
|
},
|
||||||
enabled: callerEnabled && !!user?.pubkey && hasNip44,
|
enabled: callerEnabled && !!user?.pubkey && hasNip44,
|
||||||
staleTime: interactive ? Infinity : 60000,
|
staleTime: interactive ? Infinity : 60000,
|
||||||
|
// Keep the decrypted stash in memory for the session so navigating away and
|
||||||
|
// back doesn't re-open Amber (gcTime would otherwise evict it after 5 min).
|
||||||
|
gcTime: interactive ? Infinity : undefined,
|
||||||
|
refetchOnMount: !interactive,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: !interactive,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useNostr } from '@nostrify/react';
|
||||||
import { useNostrLogin } from '@nostrify/react/login';
|
import { useNostrLogin } from '@nostrify/react/login';
|
||||||
import { useCurrentUser } from './useCurrentUser';
|
import { useCurrentUser } from './useCurrentUser';
|
||||||
import { useNostrPublish } from './useNostrPublish';
|
import { useNostrPublish } from './useNostrPublish';
|
||||||
import { hasNip44 as checkNip44 } from '@/lib/signerCapabilities';
|
import { hasNip44 as checkNip44, isInteractiveSigner } from '@/lib/signerCapabilities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* useSyncedAppData — a small engine for storing a blob of app state as ONE
|
* useSyncedAppData — a small engine for storing a blob of app state as ONE
|
||||||
|
|
@ -68,6 +68,14 @@ export function useSyncedAppData<T>(options: UseSyncedAppDataOptions<T>) {
|
||||||
const hasNip44 = useMemo(() => checkNip44(user, logins), [user, logins]);
|
const hasNip44 = useMemo(() => checkNip44(user, logins), [user, logins]);
|
||||||
const canSync = !!user?.pubkey && hasNip44;
|
const canSync = !!user?.pubkey && hasNip44;
|
||||||
|
|
||||||
|
// Amber (NIP-55) opens the signer app on EVERY nip44 decrypt. This engine is
|
||||||
|
// used by chrome that is mounted per page (e.g. the notification bell's read
|
||||||
|
// state), so re-decrypting on every navigation spams Amber popups. For an
|
||||||
|
// interactive signer, decrypt once per session and keep it cached — remounts
|
||||||
|
// read the cache instead of prompting. (Non-interactive signers are cheap, so
|
||||||
|
// they keep normal refetching.)
|
||||||
|
const interactive = isInteractiveSigner(user, logins);
|
||||||
|
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => ['synced-app-data', dTag, user?.pubkey] as const,
|
() => ['synced-app-data', dTag, user?.pubkey] as const,
|
||||||
[dTag, user?.pubkey],
|
[dTag, user?.pubkey],
|
||||||
|
|
@ -77,8 +85,13 @@ export function useSyncedAppData<T>(options: UseSyncedAppDataOptions<T>) {
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
enabled: enabled && canSync,
|
enabled: enabled && canSync,
|
||||||
staleTime: 60_000,
|
// For interactive signers: never consider it stale and never evict it from
|
||||||
|
// memory during the session → one decrypt, then cache hits.
|
||||||
|
staleTime: interactive ? Infinity : 60_000,
|
||||||
|
gcTime: interactive ? Infinity : undefined,
|
||||||
|
refetchOnMount: !interactive,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: !interactive,
|
||||||
queryFn: async ({ signal }): Promise<T> => {
|
queryFn: async ({ signal }): Promise<T> => {
|
||||||
if (!user?.pubkey || !hasNip44) return empty;
|
if (!user?.pubkey || !hasNip44) return empty;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue