v0.1.4: fix silent failure on zones that reject ParkingEstimateMulti
All checks were successful
build-apk / build (push) Successful in 14m27s
All checks were successful
build-apk / build (push) Successful in 14m27s
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 <noreply@anthropic.com>
This commit is contained in:
parent
30f32bc494
commit
f8d9e2970c
2 changed files with 94 additions and 9 deletions
|
|
@ -3,14 +3,14 @@
|
||||||
"name": "BigBrainParking",
|
"name": "BigBrainParking",
|
||||||
"slug": "bigbrainparking",
|
"slug": "bigbrainparking",
|
||||||
"scheme": "bigbrainparking",
|
"scheme": "bigbrainparking",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"android": {
|
"android": {
|
||||||
"package": "top.mowden.bigbrainparking",
|
"package": "top.mowden.bigbrainparking",
|
||||||
"versionCode": 3,
|
"versionCode": 4,
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,45 @@ function fmtDuration(min?: number): string {
|
||||||
}
|
}
|
||||||
const money = (v?: string | number) => `$${Number(v ?? 0).toFixed(2)}`;
|
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<ParkingDetail[]> {
|
||||||
|
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<number>();
|
||||||
|
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. */
|
/** Parse the API's "MM-DD-YYYY hh:mm AM" end-time string into a Date for reminders. */
|
||||||
function parseApiTime(s?: string): Date | null {
|
function parseApiTime(s?: string): Date | null {
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
|
|
@ -67,6 +106,7 @@ export function StartSessionScreen() {
|
||||||
const [selIdx, setSelIdx] = useState(0);
|
const [selIdx, setSelIdx] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [paying, setPaying] = useState(false);
|
const [paying, setPaying] = useState(false);
|
||||||
|
const [ladderError, setLadderError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load the account's vehicles + cards and pick the defaults.
|
// Load the account's vehicles + cards and pick the defaults.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -88,16 +128,41 @@ export function StartSessionScreen() {
|
||||||
const loadLadder = useCallback(async () => {
|
const loadLadder = useCallback(async () => {
|
||||||
if (vehicleId == null || space?.SpaceId == null) return;
|
if (vehicleId == null || space?.SpaceId == null) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setLadderError(null);
|
||||||
const est = await ps.getParkingEstimateMulti({
|
const base = {
|
||||||
zoneId: zone.ZoneId!,
|
zoneId: zone.ZoneId!,
|
||||||
spaceId: space.SpaceId!,
|
spaceId: space.SpaceId!,
|
||||||
customerId: zone.CustomerId!,
|
customerId: zone.CustomerId!,
|
||||||
vehicleId,
|
vehicleId,
|
||||||
});
|
};
|
||||||
const details = est.ParkingDetail ?? [];
|
try {
|
||||||
|
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);
|
setLadder(details);
|
||||||
setSelIdx((i) => Math.min(i, Math.max(0, details.length - 1)));
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +283,26 @@ export function StartSessionScreen() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ladderError && !ladder.length) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.center, { backgroundColor: colors.bg, padding: 24 }]}>
|
||||||
|
<Text style={[styles.zone, { color: colors.text }]}>{zone.ZoneName}</Text>
|
||||||
|
<View style={[styles.freeBanner, { backgroundColor: '#fdecea', marginTop: 16, alignSelf: 'stretch' }]}>
|
||||||
|
<Text style={{ color: '#c0392b', fontWeight: '700', fontSize: 16 }}>
|
||||||
|
Couldn’t load parking options
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: colors.subtext, marginTop: 6 }}>{ladderError}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.payBtn, { backgroundColor: colors.primary, marginTop: 20, alignSelf: 'stretch' }]}
|
||||||
|
onPress={() => void loadLadder()}
|
||||||
|
>
|
||||||
|
<Text style={styles.payText}>Try again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Chip = ({
|
const Chip = ({
|
||||||
active,
|
active,
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue