From 4217e883383cc84541fad2918868970d4e31addd Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 13 Aug 2026 06:02:23 +0000 Subject: [PATCH 1/2] v0.6.3: hide the paid city lots from users without a ParkSmarter account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The green lots are the map's only paid category ("City lots - Paid hourly or permit"); paying for them goes through ParkSmarter, so they are no use to someone browsing without an account. Every other category is free street parking with a posted time limit and needs nothing. Gated by category rather than by an id list. Two lots were named (off Oak St and off N 3rd Ave) and then the beach ones, which together is every green lot on the map — and an id list would silently break the next time the map is regenerated from a new PDF, since ids are positional. - Paid lots are filtered out of the overlay when signed out, so they are neither drawn nor tappable. - "Park here" still detects them, so standing in one explains that it needs an account instead of reporting no parking nearby. - The area screen guards too, in case one is reached with a stale nav param. - Server carries an optional per-area requiresAccount override for a lot that turns out to take payment another way. Null means "use the category default", so an unset value can't be confused with an explicit false. The new column needs a real migration: CREATE TABLE IF NOT EXISTS does not add a column to a table that already exists, so an already-deployed server would have kept the old schema. Covered by a test that builds the pre-migration table and then opens it. Co-Authored-By: Claude Opus 5 --- README.md | 18 ++++++++--- app/app.json | 4 +-- app/src/api/parkingAreas.ts | 18 +++++++++++ app/src/screens/CityAreaScreen.tsx | 17 ++++++++-- app/src/screens/MapScreen.tsx | 34 +++++++++++++++---- server/src/app.ts | 3 ++ server/src/db.ts | 31 ++++++++++++++++-- server/test/areas.test.ts | 52 ++++++++++++++++++++++++++++++ 8 files changed, 159 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bc8e5ab..444c391 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,19 @@ 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. -**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: +**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: - **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 b00e0ad..27cdffe 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.6.2", + "version": "0.6.3", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 23, + "versionCode": 24, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/src/api/parkingAreas.ts b/app/src/api/parkingAreas.ts index 0200636..6aad019 100644 --- a/app/src/api/parkingAreas.ts +++ b/app/src/api/parkingAreas.ts @@ -40,6 +40,11 @@ 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 { @@ -76,6 +81,19 @@ 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/screens/CityAreaScreen.tsx b/app/src/screens/CityAreaScreen.tsx index 713c05e..007eb97 100644 --- a/app/src/screens/CityAreaScreen.tsx +++ b/app/src/screens/CityAreaScreen.tsx @@ -5,7 +5,8 @@ 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 } from '@/api/parkingAreas'; +import { areaDurationOptions, areaIsFree, areaRequiresAccount } from '@/api/parkingAreas'; +import { useAuth } from '@/auth/AuthContext'; import { endActiveParking, extendAreaParking, @@ -43,6 +44,7 @@ export function CityAreaScreen() { const { area, spot } = useRoute().params; const navigation = useNavigation