v0.2.0: never auto-send GPS; "Last lot" uses last session; About page rework
All checks were successful
build-apk / build (push) Successful in 11m47s
All checks were successful
build-apk / build (push) Successful in 11m47s
Location privacy (hard invariant): the device GPS is sent to the API ONLY when the user taps "My location" and searches. - Map opens on your LAST session's parking lot (the meter's coordinate from history), not your GPS. - "Last" -> "Last lot": centers on the last session's lot and searches there (~couple-mile view) — no GPS. - "Search this area" falls back to the tracked viewport center, never the GPS fix. About page: lead with a BigBrainParking open-source blurb + repo link (was showing ParkSmarter's own About text first); privacy wording updated to match; drop the now-unused getAbout fetch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
70047b3964
commit
0a6ca53c04
4 changed files with 80 additions and 41 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const [showJoel, setShowJoel] = useState(false);
|
||||
const taps = useRef(0);
|
||||
const resetTimer = useRef<ReturnType<typeof setTimeout> | 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() {
|
|||
<Text style={[styles.title, { color: colors.text }]}>About BigBrainParking</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.body, { color: colors.text }]}>{about ?? 'Loading…'}</Text>
|
||||
<Text style={[styles.body, { color: colors.text }]}>
|
||||
BigBrainParking is a free, <Text style={{ fontWeight: '700' }}>open-source</Text>,
|
||||
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.
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity onPress={() => Linking.openURL(REPO_URL)} style={{ marginTop: 12 }}>
|
||||
<Text style={[styles.link, { color: colors.primary }]}>Source code & issues ›</Text>
|
||||
<Text style={[styles.linkUrl, { color: colors.subtext }]}>{REPO_URL}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.section, { color: colors.text }]}>What gets sent to ParkSmarter (IPS)</Text>
|
||||
<Text style={[styles.body, { color: colors.text }]}>
|
||||
|
|
@ -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 <Text style={{ fontWeight: '700' }}>only</Text> 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.
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.section, { color: colors.text }]}>What it does not do</Text>
|
||||
<Text style={[styles.body, { color: colors.text }]}>
|
||||
{'\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 <Text style={{ fontWeight: '700' }}>never sent to the API</Text>{' '}
|
||||
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.
|
||||
</Text>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Coords | null>(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<Coords | null> => {
|
||||
try {
|
||||
const past = await ps.getPastParkingSessions({ currentPage: 1, pageSize: 1 });
|
||||
const s = past.Session?.[0] as Record<string, any> | 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() {
|
|||
<TouchableOpacity style={styles.pill} onPress={goToMyLocation}>
|
||||
<Text style={styles.pillText}>My location</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.pill} onPress={searchLastKnown}>
|
||||
<Text style={styles.pillText}>Last</Text>
|
||||
<TouchableOpacity style={styles.pill} onPress={searchLastSessionLot}>
|
||||
<Text style={styles.pillText}>Last lot</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue