v1.2.0: Spedn label, multi-relay community stats, GrapheneOS Near-Me fix
1. Map filter: renamed the "Bitcoin" toggle to "Spedn" (matches the toolbar button); tooltip now says "Spedn your sats". 2. Community stats (/stats): useGrowthStats now queries the same relay set as the rest of the app (community relay + user relays) via useSkateRelays instead of only the single hardcoded SKATE_RELAY. Lab profiles / spots / etc. that live on a user's own relay are now counted, so the numbers match what /lab shows. 3. Near Me on GrapheneOS: even after granting the OS location permission, the WebView's navigator.geolocation stays blocked → "permission denied". Fixed by fetching the position from the native Capacitor Geolocation plugin first (LocationManager, honours the granted permission), falling back to navigator.geolocation only if that fails. Also auto-inject ACCESS_FINE/COARSE_LOCATION into AndroidManifest via the manifest script. Bump version to 1.2.0 (versionCode 10200). Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
6fb7314f6c
commit
677ccb8a82
5 changed files with 96 additions and 41 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.com",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -29,13 +29,17 @@ if (!existsSync(manifestPath)) {
|
|||
}
|
||||
|
||||
let manifest = readFileSync(manifestPath, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
if (manifest.includes('android:scheme="nostrsigner"')) {
|
||||
console.log('[inject-amber-queries] <queries> already present — nothing to do.');
|
||||
process.exit(0);
|
||||
// Insert right before the closing </manifest> tag.
|
||||
if (!manifest.includes('</manifest>')) {
|
||||
console.error('[inject-amber-queries] No closing </manifest> tag found — aborting.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const queriesBlock = ` <!-- REQUIRED for Amber (NIP-55) on Android 11+ / GrapheneOS.
|
||||
// ── 1. Amber <queries> block ────────────────────────────────────────────────
|
||||
if (!manifest.includes('android:scheme="nostrsigner"')) {
|
||||
const queriesBlock = ` <!-- REQUIRED for Amber (NIP-55) on Android 11+ / GrapheneOS.
|
||||
Without this the OS blocks the nostrsigner intent and the signer call
|
||||
hangs forever ("Waiting for Amber"). Injected by scripts/inject-amber-queries.mjs -->
|
||||
<queries>
|
||||
|
|
@ -46,14 +50,31 @@ const queriesBlock = ` <!-- REQUIRED for Amber (NIP-55) on Android 11+ / Grap
|
|||
<package android:name="com.greenart7c3.nostrsigner" />
|
||||
</queries>
|
||||
`;
|
||||
|
||||
// Insert right before the closing </manifest> tag.
|
||||
if (!manifest.includes('</manifest>')) {
|
||||
console.error('[inject-amber-queries] No closing </manifest> tag found — aborting.');
|
||||
process.exit(1);
|
||||
manifest = manifest.replace('</manifest>', `${queriesBlock}</manifest>`);
|
||||
changed = true;
|
||||
console.log('[inject-amber-queries] ✓ Injected Amber <queries> block.');
|
||||
} else {
|
||||
console.log('[inject-amber-queries] <queries> already present.');
|
||||
}
|
||||
|
||||
manifest = manifest.replace('</manifest>', `${queriesBlock}</manifest>`);
|
||||
writeFileSync(manifestPath, manifest);
|
||||
// ── 2. Location permissions ───────────────────────────────────────────────────
|
||||
// The native Geolocation plugin can't prompt for / obtain a fix without these.
|
||||
// Missing permissions is why "Near Me" reports denied even after the user
|
||||
// thinks they granted it.
|
||||
const locationPerms = ` <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
`;
|
||||
if (!manifest.includes('ACCESS_FINE_LOCATION')) {
|
||||
manifest = manifest.replace('</manifest>', `${locationPerms}</manifest>`);
|
||||
changed = true;
|
||||
console.log('[inject-amber-queries] ✓ Injected location permissions.');
|
||||
} else {
|
||||
console.log('[inject-amber-queries] location permissions already present.');
|
||||
}
|
||||
|
||||
console.log('[inject-amber-queries] ✓ Injected Amber <queries> block into AndroidManifest.xml');
|
||||
if (changed) {
|
||||
writeFileSync(manifestPath, manifest);
|
||||
console.log('[inject-amber-queries] AndroidManifest.xml updated.');
|
||||
} else {
|
||||
console.log('[inject-amber-queries] nothing to do.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||
import {
|
||||
SKATE_SPOT_KIND,
|
||||
SKATE_SHOP_KIND,
|
||||
|
|
@ -16,7 +16,6 @@ import {
|
|||
SKATE_MUSICIAN_KIND,
|
||||
SKATE_TOUR_KIND,
|
||||
SKATE_CREW_KIND,
|
||||
SKATE_RELAY,
|
||||
} from '@/lib/types';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
|
@ -112,12 +111,15 @@ function fillMonthGaps(keys: string[]): string[] {
|
|||
// ─── Hook ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useGrowthStats() {
|
||||
const { nostr } = useNostr();
|
||||
// Query the SAME relay set the rest of the app uses (community relay + the
|
||||
// user's relays), not just the single hardcoded community relay. Otherwise
|
||||
// Lab profiles (and spots/shops/etc.) that live on a user's own relay show up
|
||||
// on /lab but are missing from the community stats counts.
|
||||
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
||||
|
||||
return useQuery<GrowthStats>({
|
||||
queryKey: ['growth-stats'],
|
||||
queryKey: ['growth-stats', getQueryKeySuffix()],
|
||||
queryFn: async () => {
|
||||
const relay = nostr.relay(SKATE_RELAY);
|
||||
const signal = AbortSignal.timeout(15000);
|
||||
|
||||
// Query all content types in parallel from the community relay.
|
||||
|
|
@ -135,19 +137,19 @@ export function useGrowthStats() {
|
|||
crewEvents,
|
||||
otherLabEvents,
|
||||
] = await Promise.all([
|
||||
relay.query([{ kinds: [SKATE_SPOT_KIND], '#t': ['skatespot'], limit: 2000 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_SHOP_KIND], limit: 2000 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [CALENDAR_EVENT_KIND], '#t': ['skateboarding'], limit: 2000 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_COACH_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_FILMER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_EDITOR_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_PHOTOGRAPHER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_BUILDER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_TOUR_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
relay.query([{ kinds: [SKATE_CREW_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_SPOT_KIND], '#t': ['skatespot'], limit: 2000 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_SHOP_KIND], limit: 2000 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [CALENDAR_EVENT_KIND], '#t': ['skateboarding'], limit: 2000 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_COACH_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_FILMER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_EDITOR_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_PHOTOGRAPHER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_BUILDER_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_TOUR_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_CREW_KIND], limit: 500 }], { signal }).catch(() => []),
|
||||
// New Lab crafts (artist, graffiti, tattoo, trainer, musician) — folded
|
||||
// into community totals/mappers without their own chart series.
|
||||
relay.query([{ kinds: [SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND], limit: 1000 }], { signal }).catch(() => []),
|
||||
querySkateContent([{ kinds: [SKATE_ARTIST_KIND, SKATE_GRAFFITI_KIND, SKATE_TATTOO_KIND, SKATE_TRAINER_KIND, SKATE_MUSICIAN_KIND], limit: 1000 }], { signal }).catch(() => []),
|
||||
]);
|
||||
|
||||
// Deduplicate addressable events (keep newest per pubkey+kind+d)
|
||||
|
|
|
|||
|
|
@ -63,15 +63,24 @@ interface CapPermissionStatus {
|
|||
location: 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale';
|
||||
coarseLocation?: 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale';
|
||||
}
|
||||
interface CapPosition {
|
||||
coords: { latitude: number; longitude: number };
|
||||
}
|
||||
/**
|
||||
* We deliberately only type the PERMISSION methods. The position methods
|
||||
* (getCurrentPosition/watchPosition) are NOT used on native because they route
|
||||
* through Google Play Services and crash de-Googled devices — see
|
||||
* getNativePosition below, which fetches the position via navigator.geolocation.
|
||||
* Permission methods are always safe. getCurrentPosition is attempted (safely
|
||||
* wrapped) because on GrapheneOS the WebView's navigator.geolocation is blocked
|
||||
* even after the OS permission is granted — so the native plugin is the only
|
||||
* way to actually get a fix there. Recent @capacitor/geolocation falls back to
|
||||
* Android LocationManager when Play Services is absent, so it no longer crashes.
|
||||
*/
|
||||
interface CapGeolocationPlugin {
|
||||
checkPermissions(): Promise<CapPermissionStatus>;
|
||||
requestPermissions(options?: { permissions?: string[] }): Promise<CapPermissionStatus>;
|
||||
getCurrentPosition(options?: {
|
||||
enableHighAccuracy?: boolean;
|
||||
timeout?: number;
|
||||
maximumAge?: number;
|
||||
}): Promise<CapPosition>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -202,11 +211,34 @@ async function getNativePosition(native: CapGeolocationPlugin): Promise<GeoCoord
|
|||
throw new GeolocationError('denied', 'Location permission denied.');
|
||||
}
|
||||
|
||||
// Permission handled natively — now fetch the position via the WebView's
|
||||
// navigator.geolocation (Android LocationManager, no Play Services). This is
|
||||
// what avoids the fatal Play-Services crash on GrapheneOS while still
|
||||
// working normally on stock Android (the WebView shares the app's granted
|
||||
// location permission).
|
||||
// Permission granted natively. Now get the actual position.
|
||||
//
|
||||
// On GrapheneOS the WebView's navigator.geolocation is BLOCKED even after the
|
||||
// OS-level permission is granted (the WebView has its own geolocation gate
|
||||
// that Capacitor doesn't open) → navigator.geolocation returns
|
||||
// PERMISSION_DENIED, which is the "granted but still denied" bug users hit.
|
||||
//
|
||||
// So we ask the NATIVE plugin for the position first — it talks to Android's
|
||||
// LocationManager directly and honours the permission we just granted.
|
||||
// Modern @capacitor/geolocation degrades to LocationManager when Play
|
||||
// Services is absent, so this no longer crashes de-Googled devices. We wrap
|
||||
// it defensively and fall back to navigator.geolocation only if it fails.
|
||||
try {
|
||||
const pos = await safeCall(() =>
|
||||
native.getCurrentPosition({ enableHighAccuracy: false, timeout: 15000, maximumAge: 60000 }),
|
||||
);
|
||||
const lat = pos?.coords?.latitude;
|
||||
const lng = pos?.coords?.longitude;
|
||||
if (typeof lat === 'number' && typeof lng === 'number' && !(lat === 0 && lng === 0)) {
|
||||
return { lat, lng };
|
||||
}
|
||||
// Bad/empty reading — fall through to the web path.
|
||||
} catch (nativePosErr) {
|
||||
console.warn('[geolocation] native getCurrentPosition failed, falling back to WebView:', nativePosErr);
|
||||
}
|
||||
|
||||
// Fallback: WebView navigator.geolocation (works on stock Android, shares
|
||||
// the app's granted permission).
|
||||
return await getWebPosition();
|
||||
} catch (err) {
|
||||
if (err instanceof GeolocationError) throw err;
|
||||
|
|
|
|||
|
|
@ -1086,7 +1086,7 @@ export default function MapPage() {
|
|||
)}
|
||||
onClick={() => setShowBtcMap(!showBtcMap)}
|
||||
>
|
||||
<Bitcoin className="h-3 w-3" /> Bitcoin
|
||||
<Bitcoin className="h-3 w-3" /> Spedn
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1282,8 +1282,8 @@ export default function MapPage() {
|
|||
<TooltipContent side="bottom" className="max-w-[200px]">
|
||||
<p className="text-xs">
|
||||
{showBtcMap
|
||||
? `Showing ${btcMapPlaces?.length || 0} Bitcoin-accepting places nearby`
|
||||
: 'Show places where you can spend your sats (via BTC Map)'
|
||||
? `Showing ${btcMapPlaces?.length || 0} places where you can Spedn your sats nearby`
|
||||
: 'Show places where you can Spedn your sats (via BTC Map)'
|
||||
}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue