From a769c887712bcfda36f9698acb0e2160df520f2c Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Thu, 28 May 2026 10:19:51 -0700 Subject: [PATCH 1/4] Drop URN-prefixed station codes from rail trip display --- src/libs/TripReservationUtils.ts | 7 ++-- tests/unit/TripReservationUtilsTest.ts | 47 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index e4badc79e01e..df10064e0549 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -336,6 +336,9 @@ function getCarReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem return reservationList; } +// Trainline-sourced rail legs put a URN (e.g. "urn:trainline:public:nloc:at000408") in `code` instead of a short station identifier — strip those so the UI doesn't render the URN as a station label. +const getRailStationShortName = (code: string | undefined) => (code && !code.startsWith('urn:') ? code : ''); + function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem[] { const reservationList: ReservationItem[] = []; @@ -362,13 +365,13 @@ function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationIte start: { date: leg.departAt.iso8601, longName: leg.originInfo.name, - shortName: leg.originInfo.code, + shortName: getRailStationShortName(leg.originInfo.code), cityName: leg.originInfo.cityName, }, end: { date: leg.arriveAt.iso8601, longName: leg.destinationInfo.name, - shortName: leg.destinationInfo.code, + shortName: getRailStationShortName(leg.destinationInfo.code), cityName: leg.destinationInfo.cityName, }, route: { diff --git a/tests/unit/TripReservationUtilsTest.ts b/tests/unit/TripReservationUtilsTest.ts index 4addabcc12a8..b138405209c1 100644 --- a/tests/unit/TripReservationUtilsTest.ts +++ b/tests/unit/TripReservationUtilsTest.ts @@ -2692,6 +2692,53 @@ describe('TripReservationUtils', () => { }); }); + describe('rail shortName sanitization', () => { + it('should drop a URN-formatted code from rail shortName', () => { + const railPnrWithUrnCodes = JSON.parse(JSON.stringify(railPnr)) as typeof railPnr; + const leg = railPnrWithUrnCodes.data.railPnr?.legInfos.at(0); + if (leg) { + leg.originInfo.code = 'urn:trainline:public:nloc:at000408'; + leg.destinationInfo.code = 'urn:trainline:public:nloc:at001685'; + } + + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [railPnrWithUrnCodes], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(1); + + const trainReservation = result.at(0)?.reservation; + expect(trainReservation?.type).toEqual(CONST.RESERVATION_TYPE.TRAIN); + expect(trainReservation?.start?.shortName).toEqual(''); + expect(trainReservation?.end?.shortName).toEqual(''); + }); + + it('should preserve a clean station code in rail shortName', () => { + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [railPnr], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(1); + + const trainReservation = result.at(0)?.reservation; + expect(trainReservation?.type).toEqual(CONST.RESERVATION_TYPE.TRAIN); + expect(trainReservation?.start?.shortName).toEqual('STX'); + expect(trainReservation?.end?.shortName).toEqual('STY'); + }); + }); + describe('getPNRReservationDataFromTripReport', () => { it('should return an empty array when there are no transactions and trip payload', () => { const report = createRandomReport(1, undefined); From 477ca297bea52a8c58c64dde58129351491fcf69 Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Thu, 28 May 2026 10:30:50 -0700 Subject: [PATCH 2/4] Route rail station labels through formatAirportInfo so empty short codes don't render as empty parens --- src/libs/TripReservationUtils.ts | 12 ++++++------ src/pages/Travel/TrainTripDetails.tsx | 9 ++++----- tests/unit/TripReservationUtilsTest.ts | 10 +++++++++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index df10064e0549..a3e0d7a69923 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -475,12 +475,12 @@ function getReservationsFromTripReport(tripReport?: Report, transactions?: Trans } function formatAirportInfo(reservationTimeDetails: ReservationTimeDetails, hideAirportCode = false): string { - const longName = reservationTimeDetails?.longName ? `${reservationTimeDetails?.longName} ` : ''; - let shortName = reservationTimeDetails?.shortName ? `${reservationTimeDetails?.shortName}` : ''; - - shortName = longName && shortName ? `(${shortName})` : shortName; - - return !hideAirportCode ? `${longName}${shortName}` : longName; + const longName = reservationTimeDetails?.longName ?? ''; + const shortName = reservationTimeDetails?.shortName ?? ''; + if (hideAirportCode || !shortName) { + return longName; + } + return longName ? `${longName} (${shortName})` : `(${shortName})`; } function getPNRReservationDataFromTripReport(tripReport?: Report, transactions?: Transaction[]): ReservationPNRData[] { diff --git a/src/pages/Travel/TrainTripDetails.tsx b/src/pages/Travel/TrainTripDetails.tsx index aae615a0c757..3296de4ea6f0 100644 --- a/src/pages/Travel/TrainTripDetails.tsx +++ b/src/pages/Travel/TrainTripDetails.tsx @@ -7,6 +7,7 @@ import UserPills from '@components/UserPills'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import {formatAirportInfo} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; import type {Reservation} from '@src/types/onyx/Transaction'; @@ -22,9 +23,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); - const trainRouteDescription = `${reservation.start.longName} (${reservation.start.shortName}) ${translate('common.conjunctionTo')} ${reservation.end.longName} (${ - reservation.end.shortName - })`; + const trainRouteDescription = `${formatAirportInfo(reservation.start)} ${translate('common.conjunctionTo')} ${formatAirportInfo(reservation.end)}`; const trainDuration = DateUtils.getFormattedDurationBetweenDates(translate, new Date(reservation.start.date), new Date(reservation.end.date)); const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; @@ -50,7 +49,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.departs')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{startDate.hour}} - helperText={`${reservation.start.longName} (${reservation.start.shortName})`} + helperText={formatAirportInfo(reservation.start)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> @@ -58,7 +57,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.arrives')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{endDate.hour}} - helperText={`${reservation.end.longName} (${reservation.end.shortName})`} + helperText={formatAirportInfo(reservation.end)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> diff --git a/tests/unit/TripReservationUtilsTest.ts b/tests/unit/TripReservationUtilsTest.ts index b138405209c1..fa0867d44dc1 100644 --- a/tests/unit/TripReservationUtilsTest.ts +++ b/tests/unit/TripReservationUtilsTest.ts @@ -1,5 +1,5 @@ /* cspell:disable */ -import {getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; +import {formatAirportInfo, getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {Pnr, TripData} from '@src/types/onyx/TripData'; import {airReservationPnrData, airReservationTravelers} from '../data/TripAirReservationData'; @@ -2719,6 +2719,14 @@ describe('TripReservationUtils', () => { expect(trainReservation?.end?.shortName).toEqual(''); }); + it('should render only longName when shortName is empty (no trailing space, no empty parens)', () => { + expect(formatAirportInfo({date: '', longName: 'Brockenhurst', shortName: '', cityName: ''})).toEqual('Brockenhurst'); + }); + + it('should render longName with parenthesized shortName when both are present', () => { + expect(formatAirportInfo({date: '', longName: 'Solana Beach station', shortName: 'SOL', cityName: ''})).toEqual('Solana Beach station (SOL)'); + }); + it('should preserve a clean station code in rail shortName', () => { const report = createRandomReport(1, undefined); report.tripData = { From 57838543468c2e5aa7f3a6a588021f7c317c2146 Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Thu, 28 May 2026 10:31:16 -0700 Subject: [PATCH 3/4] Reword rail URN comment to clear cspell --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index a3e0d7a69923..dbf357aa262e 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -336,7 +336,7 @@ function getCarReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem return reservationList; } -// Trainline-sourced rail legs put a URN (e.g. "urn:trainline:public:nloc:at000408") in `code` instead of a short station identifier — strip those so the UI doesn't render the URN as a station label. +// Drop Trainline URN-style codes so the trip UI doesn't render them as station labels. const getRailStationShortName = (code: string | undefined) => (code && !code.startsWith('urn:') ? code : ''); function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem[] { From ad3416a39a48eab5a35876ddb630d53d00018561 Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Thu, 28 May 2026 12:30:13 -0700 Subject: [PATCH 4/4] Rename formatAirportInfo to formatTransitLocationLabel --- src/components/ReportActionItem/TripDetailsView.tsx | 10 +++++----- src/libs/TripReservationUtils.ts | 6 +++--- src/pages/Travel/TrainTripDetails.tsx | 8 ++++---- tests/unit/TripReservationUtilsTest.ts | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 9216928033ba..235a37d7f52d 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -22,7 +22,7 @@ import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {ReservationData} from '@src/libs/TripReservationUtils'; -import {formatAirportInfo, formatCancelledDescription, getPNRReservationDataFromTripReport, getTripReservationCode, getTripReservationIcon} from '@src/libs/TripReservationUtils'; +import {formatCancelledDescription, formatTransitLocationLabel, getPNRReservationDataFromTripReport, getTripReservationCode, getTripReservationIcon} from '@src/libs/TripReservationUtils'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {Reservation} from '@src/types/onyx/Transaction'; @@ -102,18 +102,18 @@ function ReservationView({reservation, transactionID, tripRoomReportID, sequence {shouldShowArrowIcon ? ( <> - {formatAirportInfo(reservation.start)} + {formatTransitLocationLabel(reservation.start)} - {formatAirportInfo(reservation.end)} + {formatTransitLocationLabel(reservation.end)} ) : ( - {formatAirportInfo(reservation.start)} {translate('common.to').toLowerCase()} {formatAirportInfo(reservation.end)} + {formatTransitLocationLabel(reservation.start)} {translate('common.to').toLowerCase()} {formatTransitLocationLabel(reservation.end)} )} @@ -217,7 +217,7 @@ function TripDetailsView({tripRoomReport, shouldShowHorizontalRule, tripTransact if (!destinationReservation) { return ''; } - return `${translate('travel.flightTo')} ${formatAirportInfo(destinationReservation.reservation.end, true)}`; + return `${translate('travel.flightTo')} ${formatTransitLocationLabel(destinationReservation.reservation.end, true)}`; } case CONST.RESERVATION_TYPE.TRAIN: if (reservations.length === 2 && firstReservation.start.shortName === lastReservation.end.shortName) { diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index dbf357aa262e..5c18405ed428 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -474,10 +474,10 @@ function getReservationsFromTripReport(tripReport?: Report, transactions?: Trans return []; } -function formatAirportInfo(reservationTimeDetails: ReservationTimeDetails, hideAirportCode = false): string { +function formatTransitLocationLabel(reservationTimeDetails: ReservationTimeDetails, hideShortCode = false): string { const longName = reservationTimeDetails?.longName ?? ''; const shortName = reservationTimeDetails?.shortName ?? ''; - if (hideAirportCode || !shortName) { + if (hideShortCode || !shortName) { return longName; } return longName ? `${longName} (${shortName})` : `(${shortName})`; @@ -563,7 +563,7 @@ export { getReservationsFromTripReport, getTripTotal, getReservationDetailsFromSequence, - formatAirportInfo, + formatTransitLocationLabel, getPNRReservationDataFromTripReport, getAirReservations, isPnrCancelled, diff --git a/src/pages/Travel/TrainTripDetails.tsx b/src/pages/Travel/TrainTripDetails.tsx index 3296de4ea6f0..2c1aaeb0b27a 100644 --- a/src/pages/Travel/TrainTripDetails.tsx +++ b/src/pages/Travel/TrainTripDetails.tsx @@ -7,7 +7,7 @@ import UserPills from '@components/UserPills'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; -import {formatAirportInfo} from '@libs/TripReservationUtils'; +import {formatTransitLocationLabel} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; import type {Reservation} from '@src/types/onyx/Transaction'; @@ -23,7 +23,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); - const trainRouteDescription = `${formatAirportInfo(reservation.start)} ${translate('common.conjunctionTo')} ${formatAirportInfo(reservation.end)}`; + const trainRouteDescription = `${formatTransitLocationLabel(reservation.start)} ${translate('common.conjunctionTo')} ${formatTransitLocationLabel(reservation.end)}`; const trainDuration = DateUtils.getFormattedDurationBetweenDates(translate, new Date(reservation.start.date), new Date(reservation.end.date)); const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; @@ -49,7 +49,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.departs')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{startDate.hour}} - helperText={formatAirportInfo(reservation.start)} + helperText={formatTransitLocationLabel(reservation.start)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> @@ -57,7 +57,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.arrives')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{endDate.hour}} - helperText={formatAirportInfo(reservation.end)} + helperText={formatTransitLocationLabel(reservation.end)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> diff --git a/tests/unit/TripReservationUtilsTest.ts b/tests/unit/TripReservationUtilsTest.ts index fa0867d44dc1..5af0a2f5b272 100644 --- a/tests/unit/TripReservationUtilsTest.ts +++ b/tests/unit/TripReservationUtilsTest.ts @@ -1,5 +1,5 @@ /* cspell:disable */ -import {formatAirportInfo, getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; +import {formatTransitLocationLabel, getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {Pnr, TripData} from '@src/types/onyx/TripData'; import {airReservationPnrData, airReservationTravelers} from '../data/TripAirReservationData'; @@ -2720,11 +2720,11 @@ describe('TripReservationUtils', () => { }); it('should render only longName when shortName is empty (no trailing space, no empty parens)', () => { - expect(formatAirportInfo({date: '', longName: 'Brockenhurst', shortName: '', cityName: ''})).toEqual('Brockenhurst'); + expect(formatTransitLocationLabel({date: '', longName: 'Brockenhurst', shortName: '', cityName: ''})).toEqual('Brockenhurst'); }); it('should render longName with parenthesized shortName when both are present', () => { - expect(formatAirportInfo({date: '', longName: 'Solana Beach station', shortName: 'SOL', cityName: ''})).toEqual('Solana Beach station (SOL)'); + expect(formatTransitLocationLabel({date: '', longName: 'Solana Beach station', shortName: 'SOL', cityName: ''})).toEqual('Solana Beach station (SOL)'); }); it('should preserve a clean station code in rail shortName', () => {