diff --git a/README.md b/README.md index 444c391..bc8e5ab 100644 --- a/README.md +++ b/README.md @@ -56,19 +56,11 @@ The **City map** layer on the Map tab is the City of Sandpoint's printed *Downto Waterfront Public Parking* map, georeferenced and drawn in the same colours as the legend: 2-hour free, 3-hour, 4-hour, no time limit, and the paid city lots. 49 areas in all. -**The free areas never touch ParkSmarter.** They live in the local database (bundled with -the app, refreshed from the zone-labels server, cached on-device), the countdown is the -phone's own clock, and the notification is the same foreground service every other session -uses. So tracking your time on a free city spot works with no account, no signal, no -payment, and in Anonymous Mode. - -The **green city lots are the exception** — they're the map's only paid category, and paying -for them means ParkSmarter. They're hidden entirely when you're not signed in, since parking -you can't actually buy is worse than no parking at all. (Standing in one and tapping "Park -here" says so rather than reporting nothing nearby.) A single lot can be flipped back via -the server's `requiresAccount` field if it turns out to take payment another way. - -Two ways to start: +**None of it touches ParkSmarter.** The areas live in the local database (bundled with the +app, refreshed from the zone-labels server, cached on-device), the countdown is the phone's +own clock, and the notification is the same foreground service every other session uses. So +tracking your time on a city spot works with no account, no signal, no payment, and in +Anonymous Mode. Two ways to start: - **Park here** — pins your car from GPS and works out which area you're in. No GPS fix (garage, indoors, radio off)? It asks you to tap the spot instead and pins that. The pin diff --git a/app/app.json b/app/app.json index 361913f..b00e0ad 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.6.4", + "version": "0.6.2", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 25, + "versionCode": 23, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/src/api/parkingAreas.ts b/app/src/api/parkingAreas.ts index 6aad019..0200636 100644 --- a/app/src/api/parkingAreas.ts +++ b/app/src/api/parkingAreas.ts @@ -40,11 +40,6 @@ export interface ParkingArea { color: string; shape: 'line' | 'polygon'; geometry: AreaGeometry; - /** - * Overrides the by-category default in [areaRequiresAccount]. Only set this to - * correct a specific lot — e.g. one that turns out to be kiosk- or permit-only. - */ - requiresAccount?: boolean; } export interface AreaData { @@ -81,19 +76,6 @@ export function areaIsFree(kind: AreaKind): boolean { return kind !== 'green_lot'; } -/** - * Whether you need a ParkSmarter account to park here. - * - * The city lots are the map's only paid category ("City lots — Paid hourly or - * permit"); paying for them means ParkSmarter, so they're no use to someone - * browsing without an account. Everything else is free with a posted time limit - * and needs nothing. A single lot can override this if it turns out to take - * payment some other way. - */ -export function areaRequiresAccount(area: ParkingArea): boolean { - return area.requiresAccount ?? area.kind === 'green_lot'; -} - /** * Durations offered when starting tracking, the posted limit first. * diff --git a/app/src/features/location/useLocation.ts b/app/src/features/location/useLocation.ts index c546634..43eb230 100644 --- a/app/src/features/location/useLocation.ts +++ b/app/src/features/location/useLocation.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import * as Location from 'expo-location'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -7,20 +7,6 @@ export interface Coords { longitude: number; } -export interface UseLocationOptions { - /** - * Re-read the OS fix this often, in ms. Omit (or 0) for a single fix at mount. - * A fix goes stale as soon as you drive a block, so any screen that shows - * "where am I" for more than a moment wants this. - */ - intervalMs?: number; - /** - * Poll only while true. Callers pass screen focus AND app foreground: polling - * a map nobody is looking at spends battery on an answer no one reads. - */ - active?: boolean; -} - const LAST_LOC_KEY = 'ps_last_location'; /** Persist the most recent fix so the "near my last location" button works cold. */ @@ -35,54 +21,32 @@ export async function getLastKnownSavedLocation(): Promise { /** * Foreground location. On GrapheneOS this uses the OS location provider directly * (no Google Play Services). We prefer a fast last-known fix, then refine. - * - * `updatedAt` is when `coords` was actually read, so callers can tell a fresh fix - * from one that has been sitting there since the screen opened. */ -export function useLocation({ intervalMs = 0, active = true }: UseLocationOptions = {}) { +export function useLocation() { const [coords, setCoords] = useState(null); - const [updatedAt, setUpdatedAt] = useState(0); const [granted, setGranted] = useState(null); const [error, setError] = useState(null); - // Read inside refresh() without making it a dependency — refresh is the - // interval's callback, and a changing identity would restart the timer on - // every fix, so it would never actually reach the interval. - const haveFix = useRef(false); - const permitted = useRef(false); const refresh = useCallback(async () => { try { - if (!permitted.current) { - const { status } = await Location.requestForegroundPermissionsAsync(); - const ok = status === 'granted'; - permitted.current = ok; - setGranted(ok); - if (!ok) { - setError('Location permission denied.'); - return null; - } + const { status } = await Location.requestForegroundPermissionsAsync(); + const ok = status === 'granted'; + setGranted(ok); + if (!ok) { + setError('Location permission denied.'); + return null; } - // Only worth it before we have anything to show: on a later poll the - // last-known fix is usually older than the one we already hold, and - // publishing it would make the dot jump backwards. - if (!haveFix.current) { - const last = await Location.getLastKnownPositionAsync(); - if (last) { - const c = { latitude: last.coords.latitude, longitude: last.coords.longitude }; - haveFix.current = true; - setCoords(c); - setUpdatedAt(Date.now()); - void saveLastLocation(c); - } + const last = await Location.getLastKnownPositionAsync(); + if (last) { + const c = { latitude: last.coords.latitude, longitude: last.coords.longitude }; + setCoords(c); + void saveLastLocation(c); } const cur = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, }); const c = { latitude: cur.coords.latitude, longitude: cur.coords.longitude }; - haveFix.current = true; setCoords(c); - setUpdatedAt(Date.now()); - setError(null); void saveLastLocation(c); return c; } catch (e: any) { @@ -92,19 +56,8 @@ export function useLocation({ intervalMs = 0, active = true }: UseLocationOption }, []); useEffect(() => { - if (!active) return; - // Denied is denied — polling it every 30s just burns wake-ups to be told no. - if (granted === false) return; - // Re-activating (screen focused, app foregrounded) is exactly when the held - // fix is most likely to be stale, so read one straight away rather than - // waiting out a whole interval. void refresh(); - if (!intervalMs) return; - const id = setInterval(() => { - void refresh(); - }, intervalMs); - return () => clearInterval(id); - }, [active, granted, intervalMs, refresh]); + }, [refresh]); - return { coords, updatedAt, granted, error, refresh }; + return { coords, granted, error, refresh }; } diff --git a/app/src/screens/CityAreaScreen.tsx b/app/src/screens/CityAreaScreen.tsx index 007eb97..713c05e 100644 --- a/app/src/screens/CityAreaScreen.tsx +++ b/app/src/screens/CityAreaScreen.tsx @@ -5,8 +5,7 @@ import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/nativ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '@/navigation/RootNavigator'; import { useTheme } from '@/theme/ThemeContext'; -import { areaDurationOptions, areaIsFree, areaRequiresAccount } from '@/api/parkingAreas'; -import { useAuth } from '@/auth/AuthContext'; +import { areaDurationOptions, areaIsFree } from '@/api/parkingAreas'; import { endActiveParking, extendAreaParking, @@ -44,7 +43,6 @@ export function CityAreaScreen() { const { area, spot } = useRoute().params; const navigation = useNavigation