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
|
||||
modes. **No external map-app handoff anywhere** (product decision) and no
|
||||
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
|
||||
**MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin
|
||||
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,
|
||||
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,
|
||||
refetchOnReconnect: !interactive,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useNostr } from '@nostrify/react';
|
|||
import { useNostrLogin } from '@nostrify/react/login';
|
||||
import { useCurrentUser } from './useCurrentUser';
|
||||
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
|
||||
|
|
@ -68,6 +68,14 @@ export function useSyncedAppData<T>(options: UseSyncedAppDataOptions<T>) {
|
|||
const hasNip44 = useMemo(() => checkNip44(user, logins), [user, logins]);
|
||||
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(
|
||||
() => ['synced-app-data', dTag, user?.pubkey] as const,
|
||||
[dTag, user?.pubkey],
|
||||
|
|
@ -77,8 +85,13 @@ export function useSyncedAppData<T>(options: UseSyncedAppDataOptions<T>) {
|
|||
const { data, isLoading } = useQuery({
|
||||
queryKey,
|
||||
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,
|
||||
refetchOnReconnect: !interactive,
|
||||
queryFn: async ({ signal }): Promise<T> => {
|
||||
if (!user?.pubkey || !hasNip44) return empty;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue