diff --git a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx index c11fbcd85..dd28c4d11 100644 --- a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx @@ -58,8 +58,13 @@ export default function MCostsTab({ planner, shell }: MTabScreenProps) { const ctx: CostsCtx = useMemo(() => ({ me, tripCurrency, convert }), [me, tripCurrency, convert]) const [settlement, setSettlement] = useState(null) + // A failed settlement read leaves `settlement` null, and the final budget would + // read that as "the trip cost nobody anything", a claim we cannot make. + const [settlementError, setSettlementError] = useState(false) const loadSettlement = useCallback(() => { - budgetApi.settlement(tripId, base).then(setSettlement).catch(() => {}) + budgetApi.settlement(tripId, base) + .then(s => { setSettlement(s); setSettlementError(false) }) + .catch(() => setSettlementError(true)) }, [tripId, base]) // Mirrors CostsPanel.tsx: items reload on trip change, settlement reloads on @@ -312,7 +317,9 @@ export default function MCostsTab({ planner, shell }: MTabScreenProps) { {/* Final budget — what the trip costs each traveler; the arithmetic opens on tap */}
{t('costs.finalBudget')}
- {tripMembers.map(p => { + {settlementError ? ( +

{t('common.unknownError')}

+ ) : tripMembers.map(p => { const row = finalBudgetFor(settlement?.finalBudgets || [], p) const open = expandedFinalId === p.id return ( diff --git a/client/tests/unit/mobile/trip/MCostsTab.test.tsx b/client/tests/unit/mobile/trip/MCostsTab.test.tsx index 7e6b65c90..6c6d11d3d 100644 --- a/client/tests/unit/mobile/trip/MCostsTab.test.tsx +++ b/client/tests/unit/mobile/trip/MCostsTab.test.tsx @@ -14,7 +14,7 @@ import { resetAllStores, seedStore } from '../../../helpers/store' import { server } from '../../../helpers/msw/server' import { fireEvent, render, screen, waitFor, within } from '../../../helpers/render' -// FE-MOB-COSTT-001 to FE-MOB-COSTT-043 +// FE-MOB-COSTT-001 to FE-MOB-COSTT-044 // The add/edit expense sheet is the shared desktop-sized form; the panel only // owns when it opens and what happens on save, so it is stubbed here. @@ -646,6 +646,16 @@ describe('MCostsTab', () => { expect(within(card).queryByText('+$58.00')).not.toBeInTheDocument() }) + it('FE-MOB-COSTT-044: a settlement read that fails says so instead of costing everyone nothing', async () => { + serveSettlement(SETTLEMENT, true) + render() + + const card = up(await screen.findByText('costs.finalBudget'), 1) + await waitFor(() => expect(within(card).getByText('common.unknownError')).toBeInTheDocument()) + expect(within(card).queryByText('$0.00')).not.toBeInTheDocument() + expect(within(card).queryByRole('button')).not.toBeInTheDocument() + }) + it('FE-MOB-COSTT-040: the expense filters apply to payments the same way they do on desktop', async () => { const mine = { id: 503, from_user_id: 1, to_user_id: 2, amount: 8, currency: 'USD', created_at: '2026-05-02T09:00:00Z' } const others = { id: 504, from_user_id: 2, to_user_id: 3, amount: 9, currency: 'USD', created_at: '2026-05-02T09:00:00Z' }