- Map is now the hero: h-[38vh] mobile / 400px desktop (was 256), with a metric
Leaflet scale control.
- New layout: flex column with header/modes/From/map pinned, the details as the
ONLY scroll region, and Start/Stop pinned at the bottom so it's always
reachable. Previously the dialog was a single overflow-hidden box, so once the
map grew everything below the fold (Start, steps) was unreachable.
- Summary rebuilt as stat tiles (distance / time / arrive) plus a detail line:
step count, honest 'uses driving roads'/'driving estimate' notes, and the
average speed the engine assumed ('21 km/h avg'; foot keeps the skate cruise
estimate). Steps header shows the count; step list scrolls taller (max-h-72).
- Wider dialog on desktop (sm:max-w-xl).
Verified in-app: tiles + 9/8/5/5 steps per mode, follow banner still tracks
movement, offline chip still appears, map 371px -> 321px with the new layout.
1.5.2 is already published to Zapstore (verified: org.skatespots.app@1.5.2 on
relay.zapstore.dev), so the post-publish work needs a new version.
- package.json 1.5.3 (-> versionName 1.5.3 / versionCode 10503).
- release-notes.txt: new v1.5.3 section (real per-mode routing + steps + follow
mode + choose-your-start + offline saved routes + Amber scoping); trimmed the
v1.5.2 section back to what was actually published.
Follow mode (explicit Start; off by default — a watcher drains battery):
watchPosition in geolocation.ts, live dot, 'NEXT · step N' banner with
distance-to-turn + distance-to-go, current step highlighted, map follows.
A transient watch error (timeout/unavailable) no longer kills it — only a
permission denial does; the watcher recovers on its own.
Choose your start: 'From → My location / Choose on map' — tapping the map sets
the origin, and the route recomputes from there.
Offline: src/lib/routeCache.ts keeps the last route per destination+mode with
the line downsampled to <=250 points. If a fetch fails (no signal / routing
server down) the dialog falls back to it with an 'Offline · saved route {age}'
badge instead of a dead error.
Also tidied the map overlays (offline badge bottom-left, Whole route
bottom-right) so nothing covers Leaflet's zoom control.
Verified with CDP: controls present; follow banner tracks movement (step 1 ->
step 7, distances update); blocking routing shows the cached route + badge;
picking a start on the map changed the route (968 m -> 922 m). Still no voice
guidance or re-routing if you leave the route (documented).
Answering 'are we giving the best navigation experience?': no, it was a preview
with dead ends. Fixes the concrete gaps:
- ETA: 'arrive ~08:10 PM' alongside the duration.
- Mode memory: the last travel mode is remembered (skatespots:route-mode), so
cyclists/drivers aren't reset to Skate/Walk every trip.
- Tappable steps: each step pans the map and highlights; added a 'Whole route'
re-fit button to zoom back out.
- Taller route map on desktop (h-64 sm:h-80).
- min-h-0 on step rows: the app-wide 44px mobile touch-target rule was making
every step a 44px slab (rows now 29px).
Still out of scope (documented, not faked): live turn-by-turn following, voice
guidance and offline routing.
AMBER — 'pops on every screen':
staleTime: Infinity only stops REFETCHING, never the FIRST decrypt. So the
encrypted useSyncedAppData blobs (read-state, preferences) each ran a queryFn on
mount (racing consumers multiplied it) and relay responses landing during
navigation made it look per-page. useSyncedAppData now has interactiveNetwork
(default false): for an interactive signer the blob is LOCAL-ONLY — query not
even enabled, patch() a no-op — no decrypt on mount, no publish on a nav-tab
change. Verified with a mocked x-amber login: network-on => Amber op at startup,
with the fix => 0. (useCrewDismissals/useStash were already gated.)
ROUTING — 'same time for every transport option':
router.project-osrm.org hosts ONE car dataset and ignores the profile in the URL,
so foot/bike/car returned identical numbers. Switched to the FOSSGIS per-profile
servers (routing.openstreetmap.de/routed-{foot,bike,car}); the demo server is now
only a last-resort fallback and its result is flagged approximate ('driving
estimate'). Request steps=true and surface turn-by-turn steps for orientation,
plus a '~X min on a board' skate estimate next to the walking time. Verified:
same trip => walk 13 min / bike 5 / moto+car 2, with 5-9 steps each.
Bump package.json (single source of truth → versionName 1.5.2 / versionCode
10502 via scripts/sync-android-version.mjs) and add the v1.5.2 release notes:
marketplace deck ratings + seller dashboard, working in-app directions, nav
fixes (desktop More / mobile Marketplace), the native mints fix, and the
newsletter CPU + relay fixes.
Patch bump over the unreleased 1.5.0, folding in today's mobile fixes: Mints in
the phone nav, instant Directions, and no stray signer prompt for the AI balance.
Sasquatch Pro (AI), in-app directions (transit/bike/moto/car), Barbers &
Hairstylists in The Lab, and a smoother app shell (no remounts, no stray
signer pop-ups).
Fixes the audit findings:
HIGH-1 (NWC/WebLN auto-pay of unverified invoices): add assertInvoiceAmount and
call it before every programmatic payment in useZaps, useCrewZap, and
SpamFeePayment. A malicious LNURL/Lightning-address server can no longer return
an inflated invoice and drain the wallet — amount is decoded from bolt11 and must
match the intended sats (±1 sat), amountless invoices are rejected.
HIGH-2 (forgeable trust signals):
- useSasquatchPro: Pro is granted ONLY by a zap receipt AUTHORED by the
platform's Lightning provider, embedding a user-signed zap request that tags
the platform, with a real paid amount >= smallest tier. The self-signed
kind:32671 receipt is now advisory only and never grants access (closed the
"self-grant permanent Pro" hole).
- useFeaturedVerification: a listing is 'verified' only when the matching 9735 is
authored by the provider pubkey (not any self-published receipt).
- useCrewGoal: tally only invoice-backed (bolt11) zaps; the forgeable amount tag
is no longer summed, so goal progress can't be inflated with fake receipts.
Bump to 1.2.14.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The injected MainActivity overrode Capacitor's WebViewClient, wiping out the
client Capacitor needs to load the web layer — causing a black-screen crash on
launch. Remove the MainActivity template, its injection script, and the build
wiring. The real signing fix (setPackageName before signEvent) is unaffected and
remains in place. Bump to 1.2.10.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Amber never appeared for delete/accept-invite/zap because Capacitor's default
WebView silently drops JS-initiated intent:/nostrsigner: navigations — the
request that opens Amber never reached the OS. Login worked only because it
uses the plugin's native startActivityForResult.
Add a custom MainActivity (android-templates/MainActivity.java) that overrides
shouldOverrideUrlLoading to launch intent:/nostrsigner: URLs via startActivity()
— a real native launch that opens Amber on GrapheneOS, matching login. Injected
into the generated android/ project at build time by inject-main-activity.mjs,
wired into android:build/sync/init. Simplify the JS launcher to navigate the
WebView (now intercepted natively). Bump to 1.2.8.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1.1.2 maps to versionCode 10102 via the sync-android-version scheme.
Release notes lead with the Lab craft-filter mobile grid fix, keeping the
1.1.1 highlights since this is the release carrying all of it to Zapstore.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Rewrite release-notes.txt (was stale at v1.0.93) with the changes shipped
since then: expanded Lab crafts (Designer/Artist, Graffiti, Tattoo,
Trainer/Physio, Musician), 7-scene Crews with opt-in member visibility and
web-of-trust vouching, Music<->Events interconnect, Newsletter Drop #004,
BETA disclaimer, and the mobile Sasquatch panel overflow fix.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1.0.92 was already published to Zapstore, so move to 1.0.93. Adjusted
sync-android-version.mjs so 1.0.x maps versionCode = patch (1.0.93 -> 93),
continuing the existing published scheme instead of jumping to 10093.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Local android/ build reported 1.0.91 which is already published, so bump the
repo to 1.0.92 to stay consistent. Note: android/app/build.gradle versionCode/
versionName must be bumped locally too (cap sync does not propagate it from
package.json).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- zapstore.yaml description: add Crews, Spot Status Reports, Near Me
locator, photographers/builders in The Lab, and local tours; remove
NoGood Radio; freshen summary line.
- release-notes.txt: replace stale v1.0.62 platform-fee notes with
current v1.0.85 highlights (Near Me crash fix + fast GPS, you-are-here
pin, Spot Status Reports, camera-first Quick Snap, Amber signing fixes,
crew/Lab deletion).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Architecture:
- usePlatformFee hook: central fee config (featuredEvent/labProfile/featuredShop)
5,000 sats each, 30-day validity, localStorage persistence, owner bypass
- PlatformFeePayment component: generic reusable payment gate
auto-pay via NWC/WebLN, QR fallback, active/expired state display
all fees route to skatespots@minibits.cash
Events (CreateEventPage):
- New optional "Entry Fee" field
- Triggers 5,000 sat platform fee when filled
- Stores entry-fee + paid tags on published event
- Free events: zero friction unchanged
The Lab (Become{Coach,Filmer,Editor}Page):
- Triggers 5,000 sat fee when "Rate" field is filled
- Submit disabled with clear CTA until fee paid
- Free/volunteer profiles: zero friction unchanged
Rule: "pay only when you earn"
- No rate set → publish instantly, no fee
- Rate set → 5,000 sats to SkateSpots, valid 30 days
- Platform owners always bypass
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Toolbar:
- Slim rounded-pill search input (was tall boxy default)
- Compact icon-only view toggle pills
- Small circle filter button (red when active, badge count)
Right-side control strip:
- Layer toggles (Spots/Shops/Gear/Tours) as 36px circles
active=opaque, inactive=35% opacity — no more giant emoji FABs
- Near Me button integrated into strip (removed from top-right)
- Quick Snap in strip, same style — no more big red circle
- All transitions: translate-x-2 slide-out when overlays open
Left-side Add FAB:
- Single gradient pill (red→orange) replaces two competing circles
- Smaller footprint, less visual noise
Loading indicator:
- Replaced center-screen "Loading..." pill with slim animated
gradient bar at top edge of map (top-0 left-0 right-0 h-0.5)
Attribution:
- Moved from floating top-right to bottom-left corner
- 9px text, 50% opacity — required but invisible
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Wallet page (/wallet):
- Add BalanceHero banner at top showing Cashu balance + wallet status pills
- Cashu/Lightning tabs with colored active states (orange/yellow)
- Remove nested balance display in CashuWalletCard (now shown in hero)
- Compact balance row in CashuWalletCard
- Stacked full-width inputs on mobile (no side-by-side on small screens)
- h-11 touch targets on all inputs and buttons (44px min)
- inputMode="numeric" on number fields (prevents iOS zoom)
- Receive/Send tabs with green/red active colors for clear context
- History items now 52px min height with colored amount text
- WalletComparisonGuide stays collapsed by default
- Fix: add Nut icon import to CashuWalletCard (was crashing)
- All modals use w-[calc(100%-1rem)] for proper mobile sizing
Settings page (/settings):
- 7-tab bar now scrollable horizontally on mobile (no overflow/wrapping)
- -mx-3 bleed trick so scrollable area goes edge-to-edge
- Wallet tab buttons full-width on mobile
- Container max-w-3xl for better desktop centering
FAQ page (/faq):
- Compact hero on mobile (smaller icon, tighter padding)
- py-6 on mobile instead of py-12
CSS utilities:
- Add xs: breakpoint (375px) variants: flex, flex-row, px-4, -mx-4, gap-2, text-xs/sm, hidden
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The chebizarro npm version has a different API to the Coracle fork that
Welshman/Coracle actually use in production. Now using:
github:coracle-social/nostr-signer-capacitor-plugin#main
Correct API (from Welshman nip55.js source):
NostrSignerPlugin.setPackageName(packageName: string) // string not object
NostrSignerPlugin.getPublicKey(packageName?: string) // packageName as arg
NostrSignerPlugin.isExternalSignerInstalled(packageName?)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
All previous URL-scheme approaches (nostrsigner:, intent:) were fundamentally
wrong for a Capacitor app. JavaScript in a WebView cannot reliably dispatch
Android intents via URL navigation.
The correct solution — used by Coracle and other Capacitor-based Nostr apps —
is the nostr-signer-capacitor-plugin which communicates with Amber via native
Android intents through the Capacitor bridge:
NostrSignerPlugin.setPackageName({ packageName: 'com.greenart7c3.nostrsigner' })
NostrSignerPlugin.isExternalSignerInstalled()
NostrSignerPlugin.getPublicKey() → { npub }
This is synchronous from the JS side, handles the entire Android intent
roundtrip natively, and returns the pubkey directly. No URL encoding,
no callback URLs, no redirects, no malformed request errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
GrapheneOS Vanadium browser blocks intent: URLs from web pages as a
security hardening measure. This is why every URL format we tried
caused "malformed request" — Vanadium was blocking the intent before
Amber ever saw it.
New flow that works on ALL Android browsers including GrapheneOS:
1. Tap "Open Amber" → navigates to nostrsigner: URL with no callbackUrl
(Amber copies pubkey to clipboard when no callbackUrl is given)
2. User approves in Amber, comes back to app
3. App tries navigator.clipboard.readText() automatically on visibilitychange
4. If clipboard permission denied → shows paste input for hex pubkey
5. User pastes pubkey → logged in
No intent: scheme, no callback URL, no redirect loop. Simple and bulletproof.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Three compounding bugs fixed:
1. WRONG FORMAT: Was using nostrsigner: directly, then intent: with encoded
values. Amber README shows intent: with raw unencoded S.key=value params.
2. # IN CALLBACKURL: Our callbackUrl was https://.../#/amber-callback which
contains a # — but intent: uses # as its own delimiter (#Intent;...;end).
Having a # inside the callbackUrl value breaks the entire intent string.
Fixed: callbackUrl is now https://.../amber-callback?event= (no hash).
3. HASHROUTER MISMATCH: Amber opens /amber-callback (no hash) but HashRouter
only handles /#/route paths. Fixed in main.tsx: pre-React IIFE detects
/amber-callback path, saves ?event= to sessionStorage, then redirects to
/#/amber-callback so React Router matches AmberCallbackPage correctly.
Final correct intent URL format:
intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=https://skate-spots.org/amber-callback?event=;end
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause of "malformed request":
- buildIntentUrl() was calling encodeURIComponent() on ALL values including
callbackUrl, which was ALREADY a full URL with special chars
- This double-encoded the callbackUrl, arriving at Amber as garbage
- The intent: wrapper format was also unnecessary
Fix: back to clean nostrsigner: format per NIP-55 spec
- Only callbackUrl gets encodeURIComponent (once)
- All other params are plain query string values
- Matches exactly what NIP-55 spec shows in its example code
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- release_notes must be a file path for zsp, not inline YAML text
- add release-notes.txt with v1.0.26 changelog
- sync package.json version to 1.0.26 to match APK versionName
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>