From f95e2233c86445463cecd516f7cf24d91882b24d Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 27 Sep 2026 15:54:29 +0700 Subject: [PATCH] fix(mobile-ux): Mints in the nav, instant directions, no stray signer prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nav: MIDDLE_SLOTS 3->4 and add 'mints' to the default middle + desktop tabs, so the Cashu mints directory is reachable on the phone out of the box (also still linked from Wallet; any of the 4 slots can be swapped via long-press). - Directions: RouteDialog now seeds the origin from the last known location and draws immediately, then refines with a live fix — no more hanging on 'Finding your location…'. Shortened the web geolocation fallback 15s -> 8s. - Amber: /api/ai/status is now public (it only reports a small balance), so showing the AI balance no longer signs NIP-98 / opens the signer. The signer is reserved for actions that actually need it. --- netlify/functions/ai.mjs | 7 +++---- src/components/RouteDialog.tsx | 15 ++++++++++++--- src/hooks/useNavPreferences.ts | 11 ++++++----- src/hooks/useSasquatchAiStatus.ts | 9 +++------ src/lib/geolocation.ts | 2 +- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/netlify/functions/ai.mjs b/netlify/functions/ai.mjs index 1cc9b7f..b93a40e 100644 --- a/netlify/functions/ai.mjs +++ b/netlify/functions/ai.mjs @@ -202,10 +202,9 @@ function json(body, status, headers) { * simpler "low balance" signal than wiring a Routstr webhook, which would need a * notification channel (a signing key) we don't otherwise have server-side. */ -async function handleStatus(request, headers) { - const pubkey = verifyNip98(request.headers.get('x-sasquatch-auth')); - if (!pubkey) return json({ error: 'auth required' }, 401, headers); - if (!PLATFORM_OWNER_PUBKEYS.has(pubkey)) return json({ error: 'forbidden' }, 403, headers); +async function handleStatus(_request, headers) { + // Public on purpose: this is just a small prepaid balance, and requiring a + // NIP-98 signature would make the signer pop up merely to display a number. if (!AI_KEY) return json({ configured: false }, 200, headers); try { const r = await fetch(`${AI_BASE}/balance/info`, { diff --git a/src/components/RouteDialog.tsx b/src/components/RouteDialog.tsx index a4cce27..8c4d3b8 100644 --- a/src/components/RouteDialog.tsx +++ b/src/components/RouteDialog.tsx @@ -11,6 +11,7 @@ import { import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { getCurrentPosition, GeolocationError } from '@/lib/geolocation'; +import { getLastLocation } from '@/lib/lastLocation'; import { fetchRoute, fetchTransitRoute, @@ -123,10 +124,18 @@ export function RouteDialog({ }, []); useEffect(() => { - if (open && !origin && !locating && !locError) { - locate(); + if (!open) return; + // Seed from the last known fix so a route can be drawn immediately, then + // refine with a live reading. This is what makes opening Directions feel + // instant instead of hanging on "Finding your location…". + if (!origin) { + const cached = getLastLocation(); + if (cached) setOrigin({ lat: cached.lat, lng: cached.lng }); } - }, [open, origin, locating, locError, locate]); + locate(); + // Re-run only when the dialog opens (locate is stable). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); // Reset transient state when closed useEffect(() => { diff --git a/src/hooks/useNavPreferences.ts b/src/hooks/useNavPreferences.ts index fc7b395..e5403e8 100644 --- a/src/hooks/useNavPreferences.ts +++ b/src/hooks/useNavPreferences.ts @@ -69,10 +69,11 @@ export const NAV_CATALOG: NavTab[] = [ ]; /** Number of user-customizable middle slots. */ -export const MIDDLE_SLOTS = 3; +export const MIDDLE_SLOTS = 4; -/** Default middle tabs (matches the app's original hardcoded bar). */ -export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews']; +/** Default middle tabs. Includes Mints (Cashu mint directory) so it's reachable + * from the phone nav out of the box; users can swap any of them via long-press. */ +export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews', 'mints']; const STORAGE_KEY = 'skatespots:nav-tabs:v1'; @@ -82,8 +83,8 @@ const STORAGE_KEY = 'skatespots:nav-tabs:v1'; // order; anything not shown still lives in the "More" dropdown, so nothing is // ever lost. Map is always the first primary tab and cannot be hidden/moved. -/** Default desktop primary tabs (matches the original hardcoded header). */ -export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews']; +/** Default desktop primary tabs. */ +export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews', 'mints']; const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1'; diff --git a/src/hooks/useSasquatchAiStatus.ts b/src/hooks/useSasquatchAiStatus.ts index 1271665..fd9d120 100644 --- a/src/hooks/useSasquatchAiStatus.ts +++ b/src/hooks/useSasquatchAiStatus.ts @@ -2,7 +2,6 @@ import { useQuery } from '@tanstack/react-query'; import { Capacitor } from '@capacitor/core'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; -import { buildProAuthHeaders } from '@/lib/proAuth'; /** Report shape from the proxy's owner-only `/api/ai/status`. */ export interface SasquatchAiStatus { @@ -31,13 +30,11 @@ export function useSasquatchAiStatus(options: { enabled?: boolean } = {}) { refetchInterval: 5 * 60 * 1000, staleTime: 60 * 1000, queryFn: async () => { - const authHeaders = await buildProAuthHeaders({ - pubkey: user!.pubkey, - signEvent: (e) => user!.signer.signEvent(e), - }); + // No signature required — see handleStatus(); avoids an Amber prompt just + // to show a balance. const res = await fetch(AI_STATUS_URL, { method: 'POST', - headers: { 'Content-Type': 'application/json', ...authHeaders }, + headers: { 'Content-Type': 'application/json' }, body: '{}', signal: AbortSignal.timeout(10000), }); diff --git a/src/lib/geolocation.ts b/src/lib/geolocation.ts index 9ec71d2..5c6d6ba 100644 --- a/src/lib/geolocation.ts +++ b/src/lib/geolocation.ts @@ -340,7 +340,7 @@ function getWebPosition(): Promise { settled = true; cleanup(); reject(new GeolocationError('timeout', 'Location request timed out.')); - }, 15000); + }, 8000); // 1) watchPosition — first update wins (fast network fix). try {