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:
parent
7969e8e9de
commit
f95e2233c8
5 changed files with 25 additions and 19 deletions
|
|
@ -202,10 +202,9 @@ function json(body, status, headers) {
|
||||||
* simpler "low balance" signal than wiring a Routstr webhook, which would need a
|
* 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.
|
* notification channel (a signing key) we don't otherwise have server-side.
|
||||||
*/
|
*/
|
||||||
async function handleStatus(request, headers) {
|
async function handleStatus(_request, headers) {
|
||||||
const pubkey = verifyNip98(request.headers.get('x-sasquatch-auth'));
|
// Public on purpose: this is just a small prepaid balance, and requiring a
|
||||||
if (!pubkey) return json({ error: 'auth required' }, 401, headers);
|
// NIP-98 signature would make the signer pop up merely to display a number.
|
||||||
if (!PLATFORM_OWNER_PUBKEYS.has(pubkey)) return json({ error: 'forbidden' }, 403, headers);
|
|
||||||
if (!AI_KEY) return json({ configured: false }, 200, headers);
|
if (!AI_KEY) return json({ configured: false }, 200, headers);
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${AI_BASE}/balance/info`, {
|
const r = await fetch(`${AI_BASE}/balance/info`, {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getCurrentPosition, GeolocationError } from '@/lib/geolocation';
|
import { getCurrentPosition, GeolocationError } from '@/lib/geolocation';
|
||||||
|
import { getLastLocation } from '@/lib/lastLocation';
|
||||||
import {
|
import {
|
||||||
fetchRoute,
|
fetchRoute,
|
||||||
fetchTransitRoute,
|
fetchTransitRoute,
|
||||||
|
|
@ -123,10 +124,18 @@ export function RouteDialog({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && !origin && !locating && !locError) {
|
if (!open) return;
|
||||||
locate();
|
// 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
|
// Reset transient state when closed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,11 @@ export const NAV_CATALOG: NavTab[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Number of user-customizable middle slots. */
|
/** 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). */
|
/** Default middle tabs. Includes Mints (Cashu mint directory) so it's reachable
|
||||||
export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews'];
|
* 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';
|
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
|
// 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.
|
// ever lost. Map is always the first primary tab and cannot be hidden/moved.
|
||||||
|
|
||||||
/** Default desktop primary tabs (matches the original hardcoded header). */
|
/** Default desktop primary tabs. */
|
||||||
export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews'];
|
export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews', 'mints'];
|
||||||
|
|
||||||
const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1';
|
const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
|
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
|
||||||
import { buildProAuthHeaders } from '@/lib/proAuth';
|
|
||||||
|
|
||||||
/** Report shape from the proxy's owner-only `/api/ai/status`. */
|
/** Report shape from the proxy's owner-only `/api/ai/status`. */
|
||||||
export interface SasquatchAiStatus {
|
export interface SasquatchAiStatus {
|
||||||
|
|
@ -31,13 +30,11 @@ export function useSasquatchAiStatus(options: { enabled?: boolean } = {}) {
|
||||||
refetchInterval: 5 * 60 * 1000,
|
refetchInterval: 5 * 60 * 1000,
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const authHeaders = await buildProAuthHeaders({
|
// No signature required — see handleStatus(); avoids an Amber prompt just
|
||||||
pubkey: user!.pubkey,
|
// to show a balance.
|
||||||
signEvent: (e) => user!.signer.signEvent(e),
|
|
||||||
});
|
|
||||||
const res = await fetch(AI_STATUS_URL, {
|
const res = await fetch(AI_STATUS_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: '{}',
|
body: '{}',
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ function getWebPosition(): Promise<GeoCoords> {
|
||||||
settled = true;
|
settled = true;
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new GeolocationError('timeout', 'Location request timed out.'));
|
reject(new GeolocationError('timeout', 'Location request timed out.'));
|
||||||
}, 15000);
|
}, 8000);
|
||||||
|
|
||||||
// 1) watchPosition — first update wins (fast network fix).
|
// 1) watchPosition — first update wins (fast network fix).
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue