diff --git a/app/app.json b/app/app.json
index 4c94380..1ac0da5 100644
--- a/app/app.json
+++ b/app/app.json
@@ -3,14 +3,14 @@
"name": "BigBrainParking",
"slug": "bigbrainparking",
"scheme": "bigbrainparking",
- "version": "0.1.7",
+ "version": "0.1.8",
"orientation": "portrait",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"icon": "./assets/icon.png",
"android": {
"package": "top.mowden.bigbrainparking",
- "versionCode": 7,
+ "versionCode": 8,
"edgeToEdgeEnabled": true,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
diff --git a/app/src/navigation/RootNavigator.tsx b/app/src/navigation/RootNavigator.tsx
index 21df729..b0295a5 100644
--- a/app/src/navigation/RootNavigator.tsx
+++ b/app/src/navigation/RootNavigator.tsx
@@ -21,14 +21,16 @@ import { VehiclesScreen } from '@/screens/VehiclesScreen';
import { PaymentMethodsScreen } from '@/screens/PaymentMethodsScreen';
import { NotificationsScreen } from '@/screens/NotificationsScreen';
import { StartSessionScreen } from '@/screens/StartSessionScreen';
+import { SessionDetailScreen } from '@/screens/SessionDetailScreen';
import { DiagnosticsScreen } from '@/screens/DiagnosticsScreen';
import { useTheme } from '@/theme/ThemeContext';
-import type { Zone } from 'parksmarter-client';
+import type { ActiveSession, PastSession, Zone } from 'parksmarter-client';
export type RootStackParamList = {
Tabs: undefined;
MeterDetail: { zone: Zone };
StartSession: { zone: Zone };
+ SessionDetail: { session: PastSession | ActiveSession; kind: 'active' | 'past' };
About: undefined;
Profile: undefined;
Vehicles: undefined;
@@ -120,6 +122,11 @@ export function RootNavigator() {
component={StartSessionScreen}
options={{ title: 'Start session' }}
/>
+
diff --git a/app/src/screens/SessionDetailScreen.tsx b/app/src/screens/SessionDetailScreen.tsx
new file mode 100644
index 0000000..eb1e770
--- /dev/null
+++ b/app/src/screens/SessionDetailScreen.tsx
@@ -0,0 +1,159 @@
+import React, { useEffect, useState } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import type { RouteProp } from '@react-navigation/native';
+import { useRoute } from '@react-navigation/native';
+import { ps } from '@/api/client';
+import { useTheme } from '@/theme/ThemeContext';
+import { logLine } from '@/features/diagnostics/fileLogger';
+import type { RootStackParamList } from '@/navigation/RootNavigator';
+import type { ParkingReceipt } from 'parksmarter-client';
+
+type DetailRoute = RouteProp;
+
+const show = (v: unknown) => (v == null || v === '' ? '—' : String(v));
+const dollars = (v: unknown) => (v == null || v === '' ? undefined : `$${v}`);
+
+export function SessionDetailScreen() {
+ const { colors } = useTheme();
+ const { session, kind } = useRoute().params;
+ const s = session as Record;
+ const tid = s.TransactionID;
+
+ const [receipt, setReceipt] = useState(null);
+ const [loading, setLoading] = useState(kind === 'past' && tid != null);
+ const [emailing, setEmailing] = useState(false);
+
+ // Past sessions have a full receipt (auth code, payment, amounts) — fetch it.
+ useEffect(() => {
+ if (kind !== 'past' || tid == null) return;
+ (async () => {
+ try {
+ const r = await ps.getParkingReceipt(tid);
+ if (r?.ParkingReceipt && (r as any).Response?.Status !== 'Error') {
+ setReceipt(r.ParkingReceipt);
+ }
+ } catch (e: any) {
+ logLine(`[RECEIPT] fetch failed tid=${tid}: ${e?.serverMessage ?? e?.message ?? e}`);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [kind, tid]);
+
+ const emailReceipt = async () => {
+ if (tid == null) return;
+ setEmailing(true);
+ try {
+ await ps.emailParkingReceipt(tid);
+ Alert.alert('Receipt sent', 'Emailed to your account address.');
+ } catch (e: any) {
+ Alert.alert('Couldn’t email receipt', e?.serverMessage ?? e?.message ?? 'Please try again.');
+ } finally {
+ setEmailing(false);
+ }
+ };
+
+ const r = receipt;
+ const rows: Array<[string, string | undefined]> =
+ kind === 'past'
+ ? [
+ ['Vehicle', r?.Vehicle ?? s.VehicleNumber],
+ ['Zone', s.Description ?? s.Zone ?? r?.MeterNumber],
+ ['Space', r?.Space ?? s.Space],
+ ['Started', r?.StartTime ?? s.StartTime],
+ ['Ended', r?.EndTime ?? s.EndTime],
+ ['Time purchased', s.TimePurchased ? `${s.TimePurchased} min` : undefined],
+ ['Parking', r?.Amount ?? dollars(s.Amount)],
+ ['Fee', r?.TransactionFee ?? dollars(s.TransactionFee)],
+ ['Total', r?.Total],
+ [
+ 'Payment',
+ [r?.PaymentDisplay ?? s.PaymentDisplay, r?.CC ?? (s.CardLastFour ? `••${s.CardLastFour}` : '')]
+ .filter(Boolean)
+ .join(' ') || undefined,
+ ],
+ ['Auth code', r?.AuthCode],
+ ['City', s.City ?? r?.CustomerName],
+ ['Transaction', tid != null ? String(tid) : undefined],
+ ]
+ : [
+ ['Vehicle', s.VehiclePlate ?? s.VehicleNumber],
+ ['Zone', s.ZoneName ?? s.Zone],
+ ['Space', s.SpaceName ?? s.Space],
+ ['Started', s.StartTimeDisplay ?? s.StartTime],
+ ['Ends', s.EndTimeDisplay ?? s.EndTime],
+ ['Time remaining', s.TimeRemaining != null ? `${s.TimeRemaining} min` : undefined],
+ ['Amount', dollars(s.Amount)],
+ ];
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const title = s.Description ?? s.ZoneName ?? s.Zone ?? r?.MeterNumber ?? 'Session';
+ const visible = rows.filter(([, v]) => v != null && v !== '');
+
+ return (
+
+ {title}
+ {kind === 'active' ? (
+
+ Active session
+
+ ) : null}
+
+
+ {visible.map(([label, value], i) => (
+
+ {label}
+ {show(value)}
+
+ ))}
+
+
+ {kind === 'past' && tid != null ? (
+
+
+ {emailing ? 'Sending…' : 'Email me this receipt'}
+
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
+ title: { fontSize: 22, fontWeight: '700', marginBottom: 12 },
+ badge: { alignSelf: 'flex-start', borderRadius: 8, paddingHorizontal: 10, paddingVertical: 4, marginBottom: 12 },
+ card: { borderRadius: 12, padding: 4, paddingHorizontal: 14 },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ paddingVertical: 10,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#8883',
+ },
+ value: { fontWeight: '600', flexShrink: 1, textAlign: 'right', marginLeft: 12 },
+ btn: { marginTop: 16, borderWidth: 1.5, borderRadius: 12, padding: 14, alignItems: 'center' },
+});
diff --git a/app/src/screens/SessionsScreen.tsx b/app/src/screens/SessionsScreen.tsx
index 6d23cf9..33d8ed0 100644
--- a/app/src/screens/SessionsScreen.tsx
+++ b/app/src/screens/SessionsScreen.tsx
@@ -1,12 +1,17 @@
import React, { useCallback, useState } from 'react';
-import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
-import { useFocusEffect } from '@react-navigation/native';
+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 { useTheme } from '@/theme/ThemeContext';
+import type { RootStackParamList } from '@/navigation/RootNavigator';
import type { ActiveSession, PastSession } from 'parksmarter-client';
+type Nav = NativeStackNavigationProp;
+
export function SessionsScreen() {
const { colors } = useTheme();
+ const navigation = useNavigation