From f8d9e2970c27bc03445dfb70560e5c2e5ea1d41c Mon Sep 17 00:00:00 2001 From: Hank Date: Thu, 9 Jul 2026 16:31:22 -0700 Subject: [PATCH] v0.1.4: fix silent failure on zones that reject ParkingEstimateMulti Some zones (e.g. short-term "DL" spaces at the downtown lot) return Response.Status:Error "Unable to process your request" for ParkingEstimateMulti even though the single-duration ParkingEstimate works. The screen swallowed that error -> empty duration list, disabled Pay button, no message ("nothing happens"). - Fall back to a single-estimate ladder across the zone's Min/Max time so these zones are purchasable (verified live: flat ~$0.10 up to ~33 min). - Surface estimate errors with a message + Try again, instead of a blank screen. - Log the estimate fallback/failure to the on-device diagnostics log. Co-Authored-By: Claude Fable 5 --- app/app.json | 4 +- app/src/screens/StartSessionScreen.tsx | 99 ++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/app/app.json b/app/app.json index 1ea5680..c552a52 100644 --- a/app/app.json +++ b/app/app.json @@ -3,14 +3,14 @@ "name": "BigBrainParking", "slug": "bigbrainparking", "scheme": "bigbrainparking", - "version": "0.1.3", + "version": "0.1.4", "orientation": "portrait", "userInterfaceStyle": "automatic", "newArchEnabled": true, "icon": "./assets/icon.png", "android": { "package": "top.mowden.bigbrainparking", - "versionCode": 3, + "versionCode": 4, "edgeToEdgeEnabled": true, "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", diff --git a/app/src/screens/StartSessionScreen.tsx b/app/src/screens/StartSessionScreen.tsx index cd65d0b..32ae51c 100644 --- a/app/src/screens/StartSessionScreen.tsx +++ b/app/src/screens/StartSessionScreen.tsx @@ -34,6 +34,45 @@ function fmtDuration(min?: number): string { } const money = (v?: string | number) => `$${Number(v ?? 0).toFixed(2)}`; +/** + * Some zones (e.g. short-term "DL" spaces) reject ParkingEstimateMulti with + * "Unable to process your request" even though the single-duration estimate + * works. Build a price ladder from ParkingEstimate (single) across the zone's + * Min/Max time so those zones stay purchasable instead of showing a blank screen. + */ +async function buildSingleLadder(base: { + zoneId: number; + spaceId: number; + customerId: number; + vehicleId: number; +}): Promise { + const probe = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: 30, creditCardId: 0 }); + if ((probe as any)?.Response?.Status === 'Error') { + throw new Error((probe as any).Response?.Message || 'Estimate unavailable for this space.'); + } + const min = Math.max(5, Number(probe.MinTime) || 5); + const max = Math.max(min, Number(probe.MaxTime) || min); + const span = max - min; + const step = span > 180 ? 30 : span > 60 ? 15 : span > 20 ? 10 : 5; + const durs = new Set(); + for (let d = min; d < max; d += step) durs.add(d); + durs.add(max); + const rungs = await Promise.all( + [...durs].map(async (d) => { + try { + const r = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: d, creditCardId: 0 }); + const p = r.ParkingDetail; + if (!p || (r as any)?.Response?.Status === 'Error') return null; + // The single endpoint often echoes Minutes:0; trust the requested duration. + return { ...p, Minutes: d } as ParkingDetail; + } catch { + return null; + } + }), + ); + return rungs.filter((r): r is ParkingDetail => r != null); +} + /** Parse the API's "MM-DD-YYYY hh:mm AM" end-time string into a Date for reminders. */ function parseApiTime(s?: string): Date | null { if (!s) return null; @@ -67,6 +106,7 @@ export function StartSessionScreen() { const [selIdx, setSelIdx] = useState(0); const [loading, setLoading] = useState(true); const [paying, setPaying] = useState(false); + const [ladderError, setLadderError] = useState(null); // Load the account's vehicles + cards and pick the defaults. useEffect(() => { @@ -88,16 +128,41 @@ export function StartSessionScreen() { const loadLadder = useCallback(async () => { if (vehicleId == null || space?.SpaceId == null) return; setLoading(true); + setLadderError(null); + const base = { + zoneId: zone.ZoneId!, + spaceId: space.SpaceId!, + customerId: zone.CustomerId!, + vehicleId, + }; try { - const est = await ps.getParkingEstimateMulti({ - zoneId: zone.ZoneId!, - spaceId: space.SpaceId!, - customerId: zone.CustomerId!, - vehicleId, - }); - const details = est.ParkingDetail ?? []; + let details: ParkingDetail[] = []; + let multiErrored = false; + try { + const est = await ps.getParkingEstimateMulti(base); + if ((est as any)?.Response?.Status === 'Error') multiErrored = true; + else details = est.ParkingDetail ?? []; + } catch { + multiErrored = true; + } + // Multi is unsupported or empty for some zones — fall back to single estimates. + if (!details.length) { + logLine( + `[ESTIMATE] multi ${multiErrored ? 'errored' : 'empty'} for zone=${base.zoneId} ` + + `space=${base.spaceId}; falling back to single`, + ); + details = await buildSingleLadder(base); + } + if (!details.length) { + throw new Error('No parking options are available for this space right now.'); + } setLadder(details); setSelIdx((i) => Math.min(i, Math.max(0, details.length - 1))); + } catch (e: any) { + const msg = e?.serverMessage ?? e?.message ?? 'Could not load parking options.'; + setLadder([]); + setLadderError(msg); + logLine(`[ESTIMATE] failed zone=${base.zoneId} space=${base.spaceId}: ${msg}`); } finally { setLoading(false); } @@ -218,6 +283,26 @@ export function StartSessionScreen() { ); } + if (ladderError && !ladder.length) { + return ( + + {zone.ZoneName} + + + Couldn’t load parking options + + {ladderError} + + void loadLadder()} + > + Try again + + + ); + } + const Chip = ({ active, label,