fix(map+badges+sasquatch): attribution contrast/overlap, dedupe 🦧, label AI balance, persist NIP-98
- Map attribution: raise contrast (grey-500/80 → grey-200/90) and add `isolate` to the map container so its internal z-indexes (Leaflet panes, overlays up to z-2000) stay inside the map — the © OpenStreetMap line was painting OVER the Sasquatch panel (z-60). Panel/bubble drop to z-40 so Radix dialogs (z-50) now sit above it instead of under it. - Sasquatch header: remove the duplicate 🦧 (the avatar + context subtitle carry it), and label the owner-only chip '⚡ AI <n>' (it is the AI credit balance, not a wallet balance). - proAuth: persist the NIP-98 auth in localStorage for its validity window, so Amber signs once per ~11h instead of on every app load (the panel itself never signed — this is the first-message auth).
This commit is contained in:
parent
359a510c65
commit
53c451ccfc
3 changed files with 63 additions and 22 deletions
|
|
@ -1474,7 +1474,7 @@ Instructions:
|
||||||
<div
|
<div
|
||||||
ref={dragRef}
|
ref={dragRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-[60] touch-none select-none',
|
'z-40 touch-none select-none',
|
||||||
// Mobile: bottom-left, clear of the bottom nav. Desktop: bottom-RIGHT
|
// Mobile: bottom-left, clear of the bottom nav. Desktop: bottom-RIGHT
|
||||||
// so the bubble is easy to spot and never hides behind the footer
|
// so the bubble is easy to spot and never hides behind the footer
|
||||||
// (which is bottom-left on desktop).
|
// (which is bottom-left on desktop).
|
||||||
|
|
@ -1513,7 +1513,7 @@ Instructions:
|
||||||
{shouldShowPanel && !isMinimized && (
|
{shouldShowPanel && !isMinimized && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed z-[60] flex flex-col transition-all duration-300 ease-out',
|
'fixed z-40 flex flex-col transition-all duration-300 ease-out',
|
||||||
// Mobile: left-anchored, max ~65% width — leaves right side free for Quick Snap + filter pills.
|
// Mobile: left-anchored, max ~65% width — leaves right side free for Quick Snap + filter pills.
|
||||||
// Height is bounded to the visible viewport (dvh) with safe-area insets so the
|
// Height is bounded to the visible viewport (dvh) with safe-area insets so the
|
||||||
// header/close button never slide off the top of the screen behind the status bar.
|
// header/close button never slide off the top of the screen behind the status bar.
|
||||||
|
|
@ -1558,7 +1558,7 @@ Instructions:
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<span className="font-black text-xs text-[#ccff00] uppercase tracking-wider" style={{ fontFamily: 'Impact, Arial Black, sans-serif' }}>
|
<span className="font-black text-xs text-[#ccff00] uppercase tracking-wider" style={{ fontFamily: 'Impact, Arial Black, sans-serif' }}>
|
||||||
🦧 Sasquatch
|
Sasquatch
|
||||||
</span>
|
</span>
|
||||||
{isPro ? (
|
{isPro ? (
|
||||||
<span
|
<span
|
||||||
|
|
@ -1583,9 +1583,9 @@ Instructions:
|
||||||
'flex items-center gap-0.5 rounded px-1 py-0.5 text-[8px] font-black uppercase tracking-wider',
|
'flex items-center gap-0.5 rounded px-1 py-0.5 text-[8px] font-black uppercase tracking-wider',
|
||||||
aiStatus.data.low ? 'bg-amber-500/20 text-amber-400' : 'bg-emerald-500/15 text-emerald-400',
|
aiStatus.data.low ? 'bg-amber-500/20 text-amber-400' : 'bg-emerald-500/15 text-emerald-400',
|
||||||
)}
|
)}
|
||||||
title={`Sasquatch AI balance — top up at ${aiStatus.data.provider ?? 'the node'}`}
|
title={`Sasquatch AI credit balance (owner-only) — top up at ${aiStatus.data.provider ?? 'the node'}`}
|
||||||
>
|
>
|
||||||
⚡ {aiStatus.data.balanceSats.toLocaleString()} sats
|
⚡ AI {aiStatus.data.balanceSats.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
* • `x-sasquatch-auth` — a base64 NIP-98 (kind 27235) event proving the caller;
|
* • `x-sasquatch-auth` — a base64 NIP-98 (kind 27235) event proving the caller;
|
||||||
* • `x-sasquatch-pro` — base64 of the Pro zap receipt proving an active pass.
|
* • `x-sasquatch-pro` — base64 of the Pro zap receipt proving an active pass.
|
||||||
*
|
*
|
||||||
* The NIP-98 event is signed ONCE per session and reused, so an interactive
|
* The NIP-98 event is signed ONCE and reused for its whole validity window —
|
||||||
* signer (Amber) doesn't pop on every message.
|
* persisted to localStorage so an interactive signer (Amber) does not pop again
|
||||||
|
* on every app load. (It expires with the 12 h window the proxy enforces.)
|
||||||
*/
|
*/
|
||||||
import type { NostrEvent } from '@nostrify/nostrify';
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
import { sasquatchAiBase } from '@/lib/sasquatchAi';
|
import { sasquatchAiBase } from '@/lib/sasquatchAi';
|
||||||
|
|
@ -23,6 +24,32 @@ type SignEvent = (event: {
|
||||||
let cachedAuthHeader: string | null = null;
|
let cachedAuthHeader: string | null = null;
|
||||||
let cachedAuthPubkey: string | null = null;
|
let cachedAuthPubkey: string | null = null;
|
||||||
|
|
||||||
|
/** Persisted NIP-98 auth (keyed to the pubkey, valid while inside the window). */
|
||||||
|
const AUTH_STORAGE_KEY = 'skatespots:sasquatch-nip98';
|
||||||
|
/** Reuse for 11 h to stay safely inside the proxy's 12 h acceptance window. */
|
||||||
|
const AUTH_MAX_AGE_SECONDS = 11 * 3600;
|
||||||
|
|
||||||
|
function loadStoredAuth(pubkey: string): string | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as { pubkey?: string; header?: string; createdAt?: number };
|
||||||
|
if (parsed?.pubkey !== pubkey || !parsed.header || !parsed.createdAt) return null;
|
||||||
|
if (Math.abs(Date.now() / 1000 - parsed.createdAt) > AUTH_MAX_AGE_SECONDS) return null;
|
||||||
|
return parsed.header;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeAuth(pubkey: string, header: string, createdAt: number): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ pubkey, header, createdAt }));
|
||||||
|
} catch {
|
||||||
|
/* private mode — the in-memory cache still works for this session */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toBase64(value: string): string {
|
function toBase64(value: string): string {
|
||||||
// Event JSON is ASCII (hex pubkeys/ids, tags), so btoa is safe here.
|
// Event JSON is ASCII (hex pubkeys/ids, tags), so btoa is safe here.
|
||||||
return btoa(value);
|
return btoa(value);
|
||||||
|
|
@ -39,9 +66,17 @@ export async function buildProAuthHeaders(opts: {
|
||||||
if (!pubkey || !signEvent) return headers;
|
if (!pubkey || !signEvent) return headers;
|
||||||
|
|
||||||
if (cachedAuthPubkey !== pubkey || !cachedAuthHeader) {
|
if (cachedAuthPubkey !== pubkey || !cachedAuthHeader) {
|
||||||
|
// Reuse a stored signature first — otherwise Amber pops on every app load
|
||||||
|
// for a signer that would happily reuse the same NIP-98 for 12 h.
|
||||||
|
const stored = loadStoredAuth(pubkey);
|
||||||
|
if (stored) {
|
||||||
|
cachedAuthHeader = stored;
|
||||||
|
cachedAuthPubkey = pubkey;
|
||||||
|
} else {
|
||||||
|
const createdAt = Math.floor(Date.now() / 1000);
|
||||||
const event = await signEvent({
|
const event = await signEvent({
|
||||||
kind: 27235,
|
kind: 27235,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: createdAt,
|
||||||
tags: [
|
tags: [
|
||||||
['u', sasquatchAiBase()],
|
['u', sasquatchAiBase()],
|
||||||
['method', 'POST'],
|
['method', 'POST'],
|
||||||
|
|
@ -50,6 +85,8 @@ export async function buildProAuthHeaders(opts: {
|
||||||
});
|
});
|
||||||
cachedAuthHeader = toBase64(JSON.stringify(event));
|
cachedAuthHeader = toBase64(JSON.stringify(event));
|
||||||
cachedAuthPubkey = pubkey;
|
cachedAuthPubkey = pubkey;
|
||||||
|
storeAuth(pubkey, cachedAuthHeader, createdAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers['x-sasquatch-auth'] = cachedAuthHeader;
|
headers['x-sasquatch-auth'] = cachedAuthHeader;
|
||||||
|
|
|
||||||
|
|
@ -1603,7 +1603,11 @@ export default function MapPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 relative">
|
{/* `isolate` creates a stacking context so the map's internal z-indexes
|
||||||
|
(Leaflet panes, our overlays up to z-[2000]) stay INSIDE the map. Without
|
||||||
|
it they competed with the root and the © OpenStreetMap attribution
|
||||||
|
(z-999) painted on top of the Sasquatch panel (z-40). */}
|
||||||
|
<div className="relative isolate flex-1">
|
||||||
{viewMode === 'map' ? (
|
{viewMode === 'map' ? (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={
|
<Suspense fallback={
|
||||||
|
|
@ -1773,10 +1777,10 @@ export default function MapPage() {
|
||||||
<div
|
<div
|
||||||
className="absolute left-1.5 z-[999] pointer-events-none bottom-[calc(5rem+env(safe-area-inset-bottom,0px)+0.25rem)] md:bottom-1.5"
|
className="absolute left-1.5 z-[999] pointer-events-none bottom-[calc(5rem+env(safe-area-inset-bottom,0px)+0.25rem)] md:bottom-1.5"
|
||||||
>
|
>
|
||||||
<div className="bg-black/40 backdrop-blur-sm px-1 py-px rounded text-[8px] text-gray-500/80 leading-none pointer-events-auto">
|
<div className="bg-black/55 backdrop-blur-sm px-1.5 py-0.5 rounded text-[9px] font-medium text-gray-200/90 leading-none pointer-events-auto">
|
||||||
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer" className="hover:text-gray-300 transition-colors">© OpenStreetMap</a>
|
<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors">© OpenStreetMap</a>
|
||||||
{showBtcMap && (
|
{showBtcMap && (
|
||||||
<>{' · '}<a href="https://btcmap.org" target="_blank" rel="noopener noreferrer" className="text-[#f7931a]/60 hover:text-[#f7931a] transition-colors">BTC Map</a></>
|
<>{' · '}<a href="https://btcmap.org" target="_blank" rel="noopener noreferrer" className="text-[#f7931a] hover:text-[#ffb04d] transition-colors">BTC Map</a></>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue