From a83611072a9a6a9a968e58e7ce12930130d5dfe4 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 27 Aug 2026 12:31:34 +0700 Subject: [PATCH] Fix Amber NIP-44 gate on Stash, mobile Crew detail layout, remove Quick Snap - signerCapabilities: recognize x-amber login type as NIP-44/NIP-04 capable (Amber implements nip44_encrypt/decrypt via native intents and URL redirects). Fixes "Encryption Required" wall on Stash for users logged in with Amber on Android. - CrewDetailPage: restructure hero header so actions wrap below the title on mobile instead of squeezing it in a cramped 3-column flex; smaller avatar/title on small screens, truncating location. - MapPage: remove Quick Snap from the mobile Add speed-dial and all its handlers, refs, web-fallback input, and now-unused imports. - zapstore.yaml: explicit orange icon + metadata_sources: [] so the correct logo is always used on the Zapstore listing. Co-authored-by: shakespeare.diy --- src/lib/signerCapabilities.ts | 6 +- src/pages/CrewDetailPage.tsx | 153 ++++++++++++++++++---------------- src/pages/MapPage.tsx | 114 +------------------------ zapstore.yaml | 12 ++- 4 files changed, 99 insertions(+), 186 deletions(-) diff --git a/src/lib/signerCapabilities.ts b/src/lib/signerCapabilities.ts index 0647493..be10240 100644 --- a/src/lib/signerCapabilities.ts +++ b/src/lib/signerCapabilities.ts @@ -36,9 +36,12 @@ export function hasNip44( if (!login) return false; // nsec + bunker: NIP-44 always available (pure JS / remote signer implements it) if (login.type === 'nsec' || login.type === 'bunker') return true; + // amber (NIP-55): the Amber signer implements nip44_encrypt / nip44_decrypt + // over native intents (CapacitorAmberSigner) and URL redirects (AmberSigner). + if (login.type === 'x-amber') return true; // extension: probe window.nostr directly (no throwing getter) if (login.type === 'extension') return !!windowNostr()?.nip44; - // amber / unknown: no NIP-44 + // unknown: no NIP-44 return false; } @@ -51,6 +54,7 @@ export function hasNip04( const login = logins.find((l) => l.pubkey === user.pubkey); if (!login) return false; if (login.type === 'nsec' || login.type === 'bunker') return true; + if (login.type === 'x-amber') return true; if (login.type === 'extension') return !!windowNostr()?.nip04; return false; } diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx index 4102086..2d05ae4 100644 --- a/src/pages/CrewDetailPage.tsx +++ b/src/pages/CrewDetailPage.tsx @@ -239,80 +239,89 @@ export default function CrewDetailPage() { {/* Hero */} -
- - - - {crew.name.slice(0, 2).toUpperCase()} - - -
-
-

- {crew.name} -

- - Crew - - - -
-
- {crew.location && ( - - {crew.location} - - )} - {crew.founded && ( - - Since {crew.founded} - - )} -
-
-
- {canManage && ( - - )} - {isFounder && ( -
- - - - +
+ {/* Top: avatar + title/meta. Actions move below on mobile so the + title never gets squeezed by the button column. */} +
+ + + + {crew.name.slice(0, 2).toUpperCase()} + + +
+
+

+ {crew.name} +

+ + Crew + + +
- )} - {isAdmin && !isFounder && ( - - You're an admin - - )} - {role === 'member' && ( - - Member - - )} - {role === 'none' && ( - - )} +
+ {crew.location && ( + + + {crew.location} + + )} + {crew.founded && ( + + Since {crew.founded} + + )} +
+
+ + {/* Actions: full-width wrap under the header, comfortable tap targets */} + {(canManage || isFounder || (isAdmin && !isFounder) || role === 'member' || role === 'none') && ( +
+ {canManage && ( + + )} + {isFounder && ( + <> + + + + + + )} + {isAdmin && !isFounder && ( + + You're an admin + + )} + {role === 'member' && ( + + Member + + )} + {role === 'none' && ( + + )} +
+ )}
{crew.bio && ( diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index cf5d5a4..2cc249a 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useRef, lazy, Suspense } from 'react'; +import { useState, useMemo, useEffect, lazy, Suspense } from 'react'; import { useSeoMeta } from '@unhead/react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Layout } from '@/components/Layout'; @@ -66,7 +66,6 @@ import { Bitcoin, SlidersHorizontal, Check, - Camera, Navigation, ExternalLink, MapPin, @@ -83,7 +82,6 @@ import { import { SpotChat } from '@/components/SpotChat'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; -import { isNativeCapture, captureFromNativeCamera } from '@/lib/quickCapture'; import { getCurrentPosition, GeolocationError, isNativeGeolocation } from '@/lib/geolocation'; @@ -603,16 +601,11 @@ export default function MapPage() { return () => window.removeEventListener('sasquatch:panelchange', handler); }, []); - // Mobile "Add" speed-dial (fans out Add Spot / Shop / Tour / Quick Snap) + // Mobile "Add" speed-dial (fans out Add Spot / Shop / Event / Gear / Tour) const [addDialOpen, setAddDialOpen] = useState(false); // Mobile layers popover const [layersOpen, setLayersOpen] = useState(false); - // Quick Snap — camera-first. On native (the APK) we open the OS camera - // directly via the runtime Camera plugin (with its built-in preview/retake). - // On web there's no reliable way to force the camera from JS, so we fall back - // to a file input hinting the rear camera. A single photo is all a spot needs. - const quickSnapInputRef = useRef(null); const { toast } = useToast(); // BTC Map center for fetching places @@ -797,80 +790,6 @@ export default function MapPage() { navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`); }; - // Quick Snap entry point — camera-first on native, file input on web. - const handleQuickSnap = async () => { - setAddDialOpen(false); - if (isNativeCapture()) { - try { - const file = await captureFromNativeCamera(); - if (file) handleQuickCameraCapture(file); - return; // handled natively (user cancel returns null → do nothing) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - console.warn('[QuickSnap] native camera failed:', msg); - // A permission error means the OS blocked the camera. Don't silently - // open the gallery — that's confusing and looks like the bug. Tell the - // user how to fix it and stop here. - if (/permission|denied|not authorized|unavailable/i.test(msg)) { - toast({ - title: 'Camera permission needed', - description: - 'Allow camera access for SkateSpots in your device Settings → Apps → SkateSpots → Permissions, then try again.', - variant: 'destructive', - }); - return; - } - // Any other native error → fall back to the photo picker so the user - // can still add a spot. - toast({ - title: 'Camera unavailable', - description: 'Opening the photo picker instead.', - }); - // fall through to the web/file-input path below - } - } - quickSnapInputRef.current?.click(); - }; - - // Web file-input change → hand the chosen photo to the upload flow. - const handleQuickSnapInput = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - // Reset the input so picking the same file again still fires onChange. - e.target.value = ''; - if (file) handleQuickCameraCapture(file); - }; - - // Handle quick camera capture — the whole point of Quick Snap is SPEED, so we - // do NOT block on the Blossom upload here (that needs an Amber signature and - // can be slow or fail — which previously left the user stranded after taking - // the photo). Instead: stash the raw photo, grab location, and navigate to the - // Add Spot form IMMEDIATELY. The form uploads the photo there. - const handleQuickCameraCapture = async (file: File) => { - try { - // Stash the captured photo as a data URL so the Create Spot page can pick - // it up and upload it in the right context. - const dataUrl = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(new Error('Could not read photo')); - reader.readAsDataURL(file); - }); - sessionStorage.setItem('quickCapturePendingPhoto', dataUrl); - // Clear any older uploaded-URL key so the form doesn't use a stale image. - sessionStorage.removeItem('quickCaptureImage'); - } catch (err) { - console.warn('[QuickSnap] could not stash photo:', err); - } - - // Try to grab location quickly, but never let it block navigation. - try { - const { lat, lng } = await getCurrentPosition(); - navigate(`/create-spot?lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}&quickCapture=true`); - } catch { - navigate('/create-spot?quickCapture=true'); - } - }; - // Filter combined spots and shops const filteredSpots = useMemo(() => { let filtered = allLocations.filter((item) => { @@ -1232,7 +1151,7 @@ export default function MapPage() {
{/* Quick actions for logged in users. - Note: Add Spot / Shop / Tour / Quick Snap live in the + Note: Add Spot / Shop / Event / Gear / Tour live in the map's "Add" speed-dial FAB — here we only surface the shortcuts that aren't already on the map (Marketplace, Stash) to avoid duplication. */} @@ -1803,19 +1722,6 @@ export default function MapPage() { ))} - {/* Quick Snap — camera-first (native) / file-input (web) */} - )} @@ -1828,7 +1734,7 @@ export default function MapPage() { ? 'bg-white rotate-45' : 'bg-[#ff0040] shadow-[#ff0040]/40 hover:bg-[#ff0040]/90' )} - aria-label={addDialOpen ? 'Close add menu' : 'Add spot, shop, tour, or snap'} + aria-label={addDialOpen ? 'Close add menu' : 'Add spot, shop, event, gear, or tour'} > @@ -1900,18 +1806,6 @@ export default function MapPage() {
- {/* Quick Snap — web fallback input (native uses the Camera plugin above). - `capture="environment"` hints the rear camera; browsers may honor it - or show a chooser. Single photo is all we need. */} - - {/* Spot Bottom Sheet — slides up when a marker is tapped, map stays visible */} { if (!open) setSelectedSpot(null); }}>