diff --git a/README.md b/README.md index 0193d45..c05e108 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ bigbrainparking/ | --- | --- | --- | | Phone + password login | ✅ wired | tokens in OS keystore | | Map of nearby meters (clickable, zoom, live GPS) | ✅ wired | MapLibre + OpenFreeMap tiles (no key) | -| "Near my last location" proximity search | ✅ wired | caches last GPS fix | +| Last-lot + My-location search | ✅ wired | opens on your last session's lot; GPS sent only via explicit "My location" | | QR kiosk scan → meter lookup | ✅ wired | on-device VisionCamera | | Save / share kiosks | ✅ wired | local (no server favorites API exists) | | Active / past sessions | ✅ wired | list views + tap for full receipt | diff --git a/app/app.json b/app/app.json index 8ab3fac..a9ec789 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.1.9", + "version": "0.2.0", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 9, + "versionCode": 10, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/src/screens/AboutScreen.tsx b/app/src/screens/AboutScreen.tsx index a036409..50c8094 100644 --- a/app/src/screens/AboutScreen.tsx +++ b/app/src/screens/AboutScreen.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Image, + Linking, Modal, Pressable, ScrollView, @@ -9,24 +10,17 @@ import { TouchableOpacity, View, } from 'react-native'; -import { ps } from '@/api/client'; import { useTheme } from '@/theme/ThemeContext'; const TAPS_TO_UNLOCK = 7; +const REPO_URL = 'https://git.mowden.top/hank/BigBrainParking'; export function AboutScreen() { const { colors } = useTheme(); - const [about, setAbout] = useState(null); const [showJoel, setShowJoel] = useState(false); const taps = useRef(0); const resetTimer = useRef | null>(null); - useEffect(() => { - ps.getAbout() - .then((r) => setAbout(r.Value ?? '')) - .catch(() => setAbout('')); - }, []); - // Tap the title 7 times (within a rolling window) to summon Joel. const onTitleTap = () => { taps.current += 1; @@ -44,7 +38,17 @@ export function AboutScreen() { About BigBrainParking - {about ?? 'Loading…'} + + BigBrainParking is a free, open-source, + de-Googled client for the ParkSmarter parking system, built to run on GrapheneOS + without any Google services. It’s an independent project — not affiliated with, + endorsed by, or supported by IPS Group / ParkSmarter. + + + Linking.openURL(REPO_URL)} style={{ marginTop: 12 }}> + Source code & issues › + {REPO_URL} + What gets sent to ParkSmarter (IPS) @@ -52,16 +56,20 @@ export function AboutScreen() { {'\n'}• Sign-in (your phone number + password) and your account data — vehicles, cards, and session history — the same as the official app. {'\n'}• To find meters, a single map coordinate: the point the map is centered on when you - search. “Near me” just centers the map on your device location first; otherwise it’s - wherever you’ve panned/zoomed to. + search. Your device GPS is sent only if you tap + “My location” to center the map on yourself and then search; otherwise it’s wherever you’ve + panned to, or your last parking lot. {'\n'}• To start a session: the meter, your vehicle and card, the times, and the amount. No location is attached. What it does not do - {'\n'}• Your phone’s GPS is used only on-device to position the map, and cached locally so - “last location” works — it is never attached to sign-in, sessions, vehicles, or payments. + {'\n'}• Your phone’s GPS is never sent to the API{' '} + unless you deliberately tap “My location” and search. The map opens on your last parking lot + (from your history — the meter’s location, not your GPS), and “Last lot” does the same. GPS + otherwise only draws your dot on the map and is never attached to sign-in, sessions, + vehicles, or payments. {'\n'}• No background location, no tracking, no device ID / IMEI / advertising identifiers, and no Google services. Session-expiry reminders are scheduled entirely on your device. @@ -84,6 +92,8 @@ const styles = StyleSheet.create({ title: { fontSize: 24, fontWeight: '700', marginBottom: 16 }, section: { fontSize: 17, fontWeight: '700', marginTop: 22, marginBottom: 6 }, body: { fontSize: 15, lineHeight: 22, color: '#333' }, + link: { fontSize: 15, fontWeight: '700' }, + linkUrl: { fontSize: 13, marginTop: 2 }, meta: { fontSize: 12, color: '#999', marginTop: 24 }, joelBackdrop: { flex: 1, diff --git a/app/src/screens/MapScreen.tsx b/app/src/screens/MapScreen.tsx index 8e5ea70..8ef643d 100644 --- a/app/src/screens/MapScreen.tsx +++ b/app/src/screens/MapScreen.tsx @@ -13,7 +13,7 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '@/theme/ThemeContext'; import { ps } from '@/api/client'; -import { useLocation, getLastKnownSavedLocation, type Coords } from '@/features/location/useLocation'; +import { useLocation, type Coords } from '@/features/location/useLocation'; import type { RootStackParamList } from '@/navigation/RootNavigator'; import type { Zone } from 'parksmarter-client'; @@ -68,11 +68,29 @@ export function MapScreen() { // location" works even when expo-location can't get a fix (e.g. indoors). const nativeFix = useRef(null); - // Seed the initial camera target from a cached location so we don't strand at 0,0. + // Look up the LAST session's parking-lot coordinate (the meter's own location + // from history — never the user's GPS). Used to open the map and by "Last lot". + const lastSessionLot = useCallback(async (): Promise => { + try { + const past = await ps.getPastParkingSessions({ currentPage: 1, pageSize: 1 }); + const s = past.Session?.[0] as Record | undefined; + const lat = Number(s?.Lat); + const lng = Number(s?.Long); + if (s && Number.isFinite(lat) && Number.isFinite(lng) && !(lat === 0 && lng === 0)) { + return { latitude: lat, longitude: lng }; + } + } catch { + /* ignore */ + } + return null; + }, []); + + // Open on your last parking lot — NOT your GPS. Your location is only ever sent + // to the API when you explicitly tap "My location", so we never auto-center on it. useEffect(() => { (async () => { - const last = await getLastKnownSavedLocation(); - setInitialCenter(last ?? coords ?? DEFAULT_CENTER); + const lot = await lastSessionLot(); + setInitialCenter(lot ?? DEFAULT_CENTER); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -109,21 +127,27 @@ export function MapScreen() { } }, []); - // Search whatever the map is currently centered on (works with no GPS). + // Search whatever the map is currently centered on. This only ever sends the + // map's center point — never the device GPS. (If you want to search your own + // location, tap "My location" to center there first, then Search this area.) const searchThisArea = async () => { + let center: [number, number] | null = null; try { - const center = await mapRef.current?.getCenter?.(); // [lng, lat] - if (center && center.length === 2) { - // Commit the current view as the camera's stop so the results re-render - // doesn't revert to the last programmatic (e.g. "My location") target. - cameraRef.current?.setCamera?.({ centerCoordinate: center, animationDuration: 0 }); - await searchAt({ latitude: center[1], longitude: center[0] }, 'this area'); - return; - } + const c = await mapRef.current?.getCenter?.(); // [lng, lat] + if (Array.isArray(c) && c.length === 2) center = [c[0], c[1]]; } catch { /* fall through */ } - if (coords) await searchAt(coords, 'this area'); + // Fallback is the tracked viewport center — deliberately NOT the GPS fix. + if (!center && viewRef.current) center = viewRef.current.center; + if (!center) { + setStatus('Move the map, then tap “Search this area”.'); + return; + } + // Commit the current view as the camera's stop so the results re-render + // doesn't revert to the last programmatic (e.g. "My location") target. + cameraRef.current?.setCamera?.({ centerCoordinate: center, animationDuration: 0 }); + await searchAt({ latitude: center[1], longitude: center[0] }, 'this area'); }; // Recenter on the live GPS fix (if available) and search there. Prefer the @@ -142,18 +166,23 @@ export function MapScreen() { await searchAt(c, 'you'); }; - const searchLastKnown = async () => { - const last = (await getLastKnownSavedLocation()) ?? coords; - if (!last) { - setStatus('No saved location yet — enable location once to cache it.'); + // Center on the LAST SESSION's parking lot (the meter's own coordinate from + // history — never your GPS) and search around it with a ~couple-mile view. + const searchLastSessionLot = async () => { + setLoading(true); + setStatus('Finding your last parking lot…'); + const lot = await lastSessionLot(); + if (!lot) { + setStatus('No past parking session with a location yet.'); + setLoading(false); return; } cameraRef.current?.setCamera?.({ - centerCoordinate: [last.longitude, last.latitude], - zoomLevel: 15, - animationDuration: 500, + centerCoordinate: [lot.longitude, lot.latitude], + zoomLevel: 12.5, // ~a couple miles across + animationDuration: 600, }); - await searchAt(last, 'last location'); + await searchAt(lot, 'your last lot'); }; // Meters as a GeoJSON layer (GPU-drawn, coordinate-anchored) — far more stable @@ -259,8 +288,8 @@ export function MapScreen() { My location - - Last + + Last lot