No decision, no code — parked with the protocol facts, recommended default
(Concord, zero-infra) vs optional Cordn/MLS upgrade, and the open questions.
The live-location marker used the SkateSpots brand logo. It now uses the
logged-in user's Nostr profile picture (their npub's logo), falling back to the
brand logo when there is none.
- SpotMap takes an optional userPicture; createUserLocationIcon layers the
avatar over the brand logo (CSS multiple backgrounds) so a broken/expired
avatar URL just reveals the logo — no inline JS, CSP-safe.
- The icon refreshes when the picture loads (or changes) without moving the pin.
- MapPage passes the current user's metadata picture.
- Export dialog shows a 4-segment strength meter (length + character classes)
with a Weak/Fair/Good/Strong label as you type the password.
- Shows the filename that will be saved; encrypted exports get an
'-encrypted' suffix so the file is self-describing at a glance.
The stash export was plaintext JSON, so a leaked/shared file exposed the whole
gear list. Export now wraps the payload in a version-2 JSON envelope:
- password mode: scrypt (NIP-49 params, 2^16/r8/p1) -> AES-256-GCM, with the
header (mode+cipher+KDF) bound as AAD so it can't be downgraded.
- nip44 mode: NIP-44-encrypted to your own key (no password to remember; needs
the matching key to open).
- plaintext v1 exports still import unchanged.
New src/lib/stashBackup.ts (verified round-trip, wrong-password, tamper and
KDF-downgrade rejection) + StashBackupExportDialog/StashBackupUnlockDialog.
The import picker only prompts for a password on password-encrypted files.
Password mode needs no signer (Amber-friendly); 'My key' is hidden unless the
signer supports NIP-44.
The marketplace Reputation tab showed a bare 'Loading...' text under
'Reviews & trade records', and TradeRecommendationsCard rendered its
'No recommendations yet.' card while still loading (then it vanished for a
self/empty view) — together they read as broken.
- Reviews/trade-records now render row-shaped skeletons while loading.
- TradeRecommendationsCard renders nothing while its query is loading when the
count is 0 (and only shows for a non-self viewer who can add one).
- StashPage: Friends only / Add to Wishlist / Add Item / Edit Item / Public
stash / Uploading… / Add Photos / placeholders / item count / list-grid aria
labels now go through t() (t added to the sub-components).
- Manual Pro copy no longer claims the pass is already active: it says the pass
activates once the provider confirms the payment.
- Bumped low-contrast gray-600 guidance text to gray-400 (DM empty state, E2EE
note, stash item count).
H14 (NWC double-pay): after the 15s pay() timeout, useNWC now polls
lookupInvoice to confirm settlement before reporting failure; if the status
stays unknown it throws a coded error (PAYMENT_STATUS_UNKNOWN) and the callers
that used to fall through to another rail (useZaps, usePurchasePro,
PlatformFeePayment) now STOP and tell the user to check their wallet — no
automatic second payment.
H1 (spot claim): claim-from now stores the ORIGINAL COORDINATE
(30818:pubkey:d) rather than an ambiguous bare pubkey; SkateSpot parses
claimedFrom/claimReason; a claimed listing shows a 'Claimed listing' notice so
the duplicate is understandable. NIP.md updated. Full reader-side supersede is
documented as the remaining design item (a wrong rule could hide real spots).
A ['featured', expiry] tag is self-asserted, so anyone could mint one and buy
top placement (the badge verified it, ordering didn't).
- New src/lib/featuredVerification.ts resolves the provider pubkey (cached) and
batch-checks provider-authored kind-9735 zap receipts for the featured coords.
- useTours / useSkateEvents / useUpcomingSkateEvents / useLabProfiles now rank
only receipts-backed featured items first.
- Availability fallback: if the provider can't be resolved or the query fails,
the self-asserted ordering is kept, so a provider outage never blanks paid
placement.
The card was role=button wrapping the Add <Button> (and a terms <a>), so Enter
on Add also opened the detail sheet. The name is now a real button and the card
no longer carries a button role.
- H13 offer threads: the seller is ALWAYS the listing author (derived from the
event pubkey), never a self-declared role tag — a non-owner can no longer
appear as the seller with a payment pointer.
- H12 trade reputation: a preimage/deposit proof counts at most once, so a real
payment can't be reused across attestations to inflate payment-verified.
- Crew alerts: listed crews are filtered against the user's own membership
confirmation labels (one batched query), so a confirmed member no longer gets
a phantom 'confirm to join' badge/notification when the crew is edited.
- Error boundaries (app + route) and DesktopNavManager are translated; the
class boundary renders a translated functional ErrorFallback. New batch4
namespaces errorsUi.* / desktopNav.*.
- Tour sponsor URL/logo sanitized before href/src (SponsorSlot) — same XSS
guard EventSponsors already uses.
- Official SkateSpots badge only shows when the NIP-05 actually verifies
(NIP19Page), not on a self-asserted handle.
- Community stats reads are null-guarded on persisted-cache fields
(monthly/cumulativeSpots/topMappers) — no more crash on an old cache.
- Overpass response no longer assumes .elements (food layer).
- user-listings query key is memoized (was Date.now() → refetch storm).
- Pro grant requires the l=pro label (was accepted with none).
- Verify card RSVP maps accepted/tentative (was going/interested → showed
'No RSVP' on a valid ticket).
- Expired listings can't be bought via a stale/shared URL.
- Batch spot photo/status hooks include the relay suffix and use
querySkateContent (relay-policy parity with useSpotReports).
- Offline queue requeues rows stuck in 'publishing' after a crash.
- querySkateContent/queryCanonical dedupe replaceable events by author+kind
(not id) so a stale profile/relay list can't win.
- Route offline cache key includes the origin (was destination+mode only).
- Internal links use <Link> (MarketplacePage); EventCountdown reacts to
language changes.
- useMarketplaceItem / useSkateEvents now pass requireAny, so a total outage
throws (isError) instead of silently returning [].
- ListingDetailPage and EventsPage render the shared ErrorState with a retry
instead of a false 'Listing not found' / 'No upcoming events'.
- H2 useAdminModeration now fetches the 'cleared' label too, so an admin Undo
actually propagates (newest-verdict-wins) instead of only working on-device.
- H3 admin takedowns publish the NIP-32 'hidden' label (useAdminHide) instead of
an author-only kind-5 relays ignore; the cross-app hide is real now.
- H5 the /stats outage banner works: queryCanonical reports relay failures via
onError, so 'partial' can be true and zeros are never presented as truth.
- H6 useShops got its own cache namespace (was colliding with useSkateShops).
- H7 listing NIP-05 is verified against the user's key, not merely self-asserted.
- H8 NIP-22 spot comments/transfer now use uppercase root tags (A/K/P); readers
accept legacy #a; NIP.md corrected.
- H9 Lab profile edits preserve foreign spot refs (crew link = 35006 only).
- H10 collection edits preserve foreign NIP-51 tags.
- H11 LAB_KINDS = all 13 provider kinds (was 35001-35005).
- H16 skip-to-content is a button that focuses <main> (was href=#main, which
HashRouter treated as a route → 404).
- H17 CustomizeNavSheet is translated; useLocalStorage re-reads on key change and
keeps stateRef in sync cross-tab.
- Backup 'confirmed' flag is now per-account (backup-confirmed:<pubkey>).
- C1 Marketplace payments now verify the invoice amount: usePayPaymentPointer
calls assertInvoiceAmount(invoice, amountSats) before paying, so a seller
can't attach a raw BOLT11 or return an inflated LNURL invoice and have an
auto-approving wallet pay it.
- C2 NIP-17 DMs now verifyEvent(seal) in all four read paths — the seal is the
only authentication for a message; a crafted seal could impersonate the sender.
- C3 push-service: bind endpoint ownership (reject subscribe by a different
pubkey; only the owner may unsubscribe) and reject non-HTTPS/private/loopback
endpoints (SSRF).
- C4 Amber callback: a bare ?event=<pubkey> is no longer treated as a login;
the callback must match a pending get_public_key op (both callback pages).
Also stops logging the pending op payload.
- C5a platform-fee record is now per-pubkey, so an account on a shared device
can't inherit another's paid status.
- C5b (self-asserted featured ranking) deferred: needs a batch-verification pass
against provider zap receipts before it can gate ordering.
- New src/lib/dateFnsI18n.ts re-exports date-fns with formatDistanceToNow /
formatDistanceToNowStrict / formatDistance / format defaulting to the active
locale, plus dateLocaleCode() for native toLocale{Date,Time}String calls.
- LanguageProvider keeps the locale in sync during render, so the first paint is
already in-language.
- Codemod: 11 date-fns imports now use the shim; ~39 native date/time calls pass
the app locale code instead of undefined/[]/hardcoded 'en-US'. Number
.toLocaleString() calls are deliberately untouched.
- VenueAccessCard now has a visitor branch: a public 'this venue issues access
passes' hint (count only, never a holder list) shown only when the venue has
active passes. Rendered for everyone on shops and gated spots.
- New shared, cached useVenuesWithPasses() hook resolves active pass coords +
owners in one query.
- SpotCard badges shops that issue passes; MarketplaceItemCard flags sellers
whose venue issues passes (title explains it is the venue, not the listing).
- i18n: venueAccess.publicTitle/publicDesc/badge/sellerHint (EN + ES).
- New owner-only VenueAccessCard on shop and gated spot pages: shows active/
revoked pass counts for that venue and links into the card studio with the
venue preselected (/card?venue=<kind>:<pubkey>:<d>).
- RiderCardPage reads the venue search param and opens the park editor with the
matching venue + type selected.
- i18n: new venueAccess.* keys (EN + ES).
- U2: CardPreview is zoomable (click for a full-screen overlay); used in the
studio, 'My passes' and the public card.
- U4: the page title is translated (Rider Card / Carné de Rider) — was hardcoded
English.
- L4: removed the legacy style/skills from RiderCardConfig + CardData (the card
derives style/level and shows only witnessed badges).
- I2: cardVerifyUrl/cardPublicUrl are clean (/verify/<naddr>, /card/<npub>) and a
startup shim rewrites those paths to the hash (old /#/ links still work).
- L2: a 'venue' can now be any spot (30818) OR shop (30017) you own — myVenues
is normalised; the pass 'a' tag uses the venue's real kind; /verify confirms
issuer ownership for spots, shops and events.
- L1: NIP.md renames kind 35679 from 'Park Access Card' to 'Access Pass' and
documents the target binding (spot / shop / event).
- B1: Share's clipboard fallback copied https://…/<npub> (a path HashRouter
ignores → homepage). Now /#/card/<npub> (native share text too).
- B4: useIssuedPasses didn't dedupe by d, so a revoked pass showed twice;
now newest-per-d like useReceivedPasses.
- B2/L5: the redesign dropped the stats row, so Spots/Zaps/Events toggles +
useRiderStats + spotsAdded/zapsReceived/eventsCreated/memberYear were dead.
Removed across lib/riderCard, CardData, CanvasCardData, both pages.
- B3: PublicCardPage ignored derived style/level (config.style is '' now) — it
now derives them from the Skater profile like the studio; holographic honors
the saved config.
- B6: holographic wasn't persisted — added to the card config/tags/parse.
- B5: 'Save PNG' is front-only while Print is both sides → label it
'Save front (PNG)' when a back exists.
The APK build failed compiling the plugin: Capacitor's `activity` is an
AppCompatActivity, so the module needs appcompat. Now `npm run android:build`
succeeds, wires skatespots-nfc in, and merges the NFC permission. Still needs a
device test for the actual write.
Web NFC isn't exposed in Capacitor's WebView, so the APK needs a native writer.
Adds packages/skatespots-nfc — a local Capacitor Android plugin (Kotlin,
enableReaderMode + NDEF URL record), registered BY NAME (not bundled). lib/nfc.ts
now uses the plugin on native and Web NFC on the web. iOS can't write tags.
⚠️ UNVERIFIED (needs cap sync + a device). README documents it.
- lib/nfc.ts: isNfcSupported() + writeNfcUrl() via Web NFC (Chrome/Android),
writing ONLY the verify URL (the same pointer the QR carries).
- NfcWriteButton component (renders nothing where Web NFC is unavailable — iOS,
desktop, Capacitor WebView — so there's no dead button).
- Wired into: the venue's issued-pass list (issuer copies a tag per holder), the
organiser's issued list, and the holder's 'My passes' view (+ Print).
- i18n nfc.* EN/ES. Tags are passive and clonable like a QR; trust still comes
from the relay signature, and /verify already handles taps.
Redesigned around the ONE question a verifier needs in half a second:
- Big verdict hero (✅ Valid / ❌ Expired / Revoked / Invalid / ⚠ Unverified
issuer / Not bound / No RSVP / Admitted / Authentic) with a plain-language line.
- Facts grid (holder, venue/organiser, valid-thru, access level) large and legible.
- Technical detail moved into a collapsed 'Details' section below.
- Combined verdict for access passes (signature + not revoked + not expired +
issuer owns the venue) and event tickets (organiser's event + holder RSVP).
No account/app needed; NFC tap and QR both land here.
The event organiser can see who they've issued check-in passes to (holder +
revoked state) and revoke them — same affordance the park panel has. (Skipped
listing a holder's passes on their public profile: a pass is between a venue
and a holder, so publishing it would leak where someone has access.)
A holder previously had no way to see a pass a venue/organiser issued them.
New useReceivedPasses (kind 35679 #p=me) + a fourth /card tab listing them;
selecting one renders it as a card (type park) with a Print button, shows the
access level / valid-thru, and flags revoked passes. Completes the
issue -> receive -> display -> verify loop.
- Duotone: desaturate + theme-accent tint the background photo so any image
reads as a branded SkateSpots card (text stays legible). Verified visually.
- Revocation: the venue owner can revoke an issued pass from the studio list;
revoke re-publishes under the same d with status=revoked (addressable
supersede — works on every relay). /verify shows a red 'Pass revoked'.
- Organiser check-in: an event's organiser can issue a signed access pass (kind
35679) bound to the event (a=31923:...); /verify confirms the issuer OWNS the
event (generalised the venue check to spots OR events) and labels it Organiser.
- useIssuedPasses/useRevokePass; i18n EN/ES.
- Attendee card = 'MY TICKET', organiser card = 'EVENT · HOST' — never present a
self-made card as organiser-issued.
- A ticket's QR now carries ?h=<holder-npub>; /verify fetches that holder's RSVP
(kind 31925, #a = the event) and shows Ticket holder + RSVP status, so a door
has a real check with no server.
- NIP.md + AGENTS updated; i18n EN/ES.
- Park Access is only offered if you own a venue (a spot you authored), and
publishing REQUIRES a venue binding — only the venue owner's key can issue it.
The pass names the holder; /verify shows holder + venue match + expiry.
- Card types are now gated by what you can actually issue/derive (Park/ Crew /
Event), with a note explaining how to unlock more.
- Park panel shows a 'map a spot you own' prompt when you have no venue; the
Publish button is disabled until a venue + holder are set.
- NIP.md: venue binding is required for new passes. i18n EN/ES.
- QR 150->178px and moved up (bottom-right, above the identity block).
- Bottom scrim now fades into the THEME's dark background + an accent glow
(was plain black) so the card reads as branded.
- Studio shows an 'add a profile photo' nudge when the account has no kind-0
picture (the card's placeholder name was the tell).
The card had a portrait panel + dashed stub divider — two vertical lines with a
dead middle. Rebuilt as one composition: full-bleed photo/cover, a scrim, the
name + identity overlaid bottom-left, and the QR floating bottom-right. No photo
falls back to a themed gradient with a monogram watermark. Applied to all four
front types; the back is a plain list. Also: DEFAULT_RIDER_CARD.style is now ''
so a profile-less card doesn't claim 'Street'. Verified by rendering a real
profile, an app-profile, and an empty npub.
The card showed the placeholder (now initials) even when the npub had a photo:
useAuthor missed profiles that only live on the community relay and could drop
a profile its strict schema rejected. Card pages now use a tolerant
useCardProfile() that reads kind 0 via querySkateContent (community + user
relays) and JSON.parses leniently. Also: useAuthor now queries the community
relay as its own parallel branch (a flaky relay in the group used to reject the
whole batch); and '_@domain' NIP-05 no longer renders a lone '@'.
Verified by rendering a real profile's card headlessly.