BigBrainParking/app/src/screens/SessionsScreen.tsx
Erik 4a5660e7e1
Some checks failed
build-apk / build (push) Failing after 1h56m52s
v0.4.0: Anonymous Mode + zone-location mirror; free check-in on any zone
Anonymous Mode — "Park without signing in" on the login screen (with a popup of
what works vs needs a login). Anonymous users browse parking areas from our
mirror, see labels, and start free check-in timers; paying, sessions, and
account screens prompt to sign in. AuthContext gains an 'anonymous' status +
enterAnonymous/requireLogin.

Zone mirror — server gains a `zones` table + public GET /api/zones and admin
POST /api/zones/sync. Signed-in admins push the zones they pull (authed) from
ParkSmarter after each map search, so anonymous users can read areas without a
ParkSmarter login. Map/Scan read the mirror when anonymous.

Also: the free "Check in" button is now always available with a 2h/3h/4h picker
(no longer gated on a prior label) — fixes "couldn't start a timer on a free
zone". CORS probe confirmed ParkSmarter allows any origin but only Content-Type,
so a future PWA can't auth to it — the mirror is what makes anonymous browsing
(and a PWA) possible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 21:38:32 +00:00

115 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { ps } from '@/api/client';
import { useAuth } from '@/auth/AuthContext';
import { useTheme } from '@/theme/ThemeContext';
import type { RootStackParamList } from '@/navigation/RootNavigator';
import type { ActiveSession, PastSession } from 'parksmarter-client';
type Nav = NativeStackNavigationProp<RootStackParamList>;
export function SessionsScreen() {
const { colors } = useTheme();
const navigation = useNavigation<Nav>();
const { isAnonymous, requireLogin } = useAuth();
const [active, setActive] = useState<ActiveSession[]>([]);
const [past, setPast] = useState<PastSession[]>([]);
const [refreshing, setRefreshing] = useState(false);
const load = useCallback(async () => {
if (isAnonymous) return;
setRefreshing(true);
try {
const [a, p] = await Promise.all([
ps.getActiveParkingSessions(),
ps.getPastParkingSessions({ currentPage: 1, pageSize: 20 }),
]);
setActive(a.ParkingSession ?? []);
setPast(p.Session ?? []);
} finally {
setRefreshing(false);
}
}, [isAnonymous]);
useFocusEffect(
useCallback(() => {
void load();
}, [load]),
);
if (isAnonymous) {
return (
<View style={[styles.center, { backgroundColor: colors.bg }]}>
<Text style={[styles.zone, { color: colors.text, marginBottom: 8 }]}>
Sign in to see your sessions
</Text>
<Text style={[styles.empty, { color: colors.subtext, textAlign: 'center', marginBottom: 16 }]}>
Your active and past parking sessions live in your ParkSmarter account.
</Text>
<TouchableOpacity
style={[styles.card, { backgroundColor: colors.primary, paddingHorizontal: 28 }]}
onPress={requireLogin}
>
<Text style={{ color: '#fff', fontWeight: '700' }}>Sign in</Text>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView
style={{ backgroundColor: colors.bg }}
contentContainerStyle={{ padding: 16 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={load} />}
>
<Text style={[styles.header, { color: colors.text }]}>Active</Text>
{active.length === 0 ? (
<Text style={[styles.empty, { color: colors.subtext }]}>No active sessions.</Text>
) : (
active.map((s, i) => (
<TouchableOpacity
key={i}
style={[styles.card, { backgroundColor: colors.primary + '22' }]}
onPress={() => navigation.navigate('SessionDetail', { session: s, kind: 'active' })}
>
<Text style={[styles.zone, { color: colors.text }]}>{s.ZoneName ?? 'Session'}</Text>
<Text style={[styles.meta, { color: colors.subtext }]}>
{s.SpaceName ?? s.Space ?? ''} · ends {s.EndTimeDisplay ?? s.EndTime ?? ''} tap for details
</Text>
</TouchableOpacity>
))
)}
<Text style={[styles.header, { color: colors.text, marginTop: 20 }]}>History</Text>
{past.length === 0 ? (
<Text style={[styles.empty, { color: colors.subtext }]}>No past sessions.</Text>
) : (
past.map((s, i) => (
<TouchableOpacity
key={i}
style={[styles.card, { backgroundColor: colors.card }]}
onPress={() => navigation.navigate('SessionDetail', { session: s, kind: 'past' })}
>
<Text style={[styles.zone, { color: colors.text }]}>
{s.Description ?? s.Zone ?? s.ZoneName ?? 'Session'}
</Text>
<Text style={[styles.meta, { color: colors.subtext }]}>
{s.StartTime ?? ''} · {s.Amount != null ? `$${s.Amount}` : ''} tap for receipt
</Text>
</TouchableOpacity>
))
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32 },
header: { fontSize: 18, fontWeight: '700', marginBottom: 8 },
empty: { marginBottom: 8 },
card: { borderRadius: 10, padding: 14, marginBottom: 10 },
zone: { fontSize: 16, fontWeight: '600' },
meta: { fontSize: 13, marginTop: 2 },
});