fix(mobile-ux): Mints in the nav, instant directions, no stray signer prompt

- 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.
This commit is contained in:
SkateSpots 2026-09-27 15:54:29 +07:00
parent 7969e8e9de
commit f95e2233c8
5 changed files with 25 additions and 19 deletions

View file

@ -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`, {

View file

@ -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(() => {

View file

@ -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';

View file

@ -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),
});

View file

@ -340,7 +340,7 @@ function getWebPosition(): Promise<GeoCoords> {
settled = true;
cleanup();
reject(new GeolocationError('timeout', 'Location request timed out.'));
}, 15000);
}, 8000);
// 1) watchPosition — first update wins (fast network fix).
try {