Some checks failed
build-apk / build (push) Failing after 1h56m52s
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>
115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
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 },
|
||
});
|