diff --git a/app/component/FareDisclaimer.js b/app/component/FareDisclaimer.js new file mode 100644 index 0000000000..5b84a4bc7e --- /dev/null +++ b/app/component/FareDisclaimer.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import Icon from './Icon'; + +const FareDisclaimer = ({ textId, values, href = null, linkText = null }) => { + return ( +
+
+ +
+
+ + + {href && ( + + + + )} +
+
+ ); +}; + +FareDisclaimer.propTypes = { + textId: PropTypes.string.isRequired, + values: PropTypes.oneOfType([ + PropTypes.shape({ + agencyName: PropTypes.string, + }), + PropTypes.object, + ]), + href: PropTypes.string, + linkText: PropTypes.string, +}; + +FareDisclaimer.defaultProps = { + values: {}, + href: null, + linkText: null, +}; + +export default FareDisclaimer; diff --git a/app/component/ItineraryTab.js b/app/component/ItineraryTab.js index 1e87bfb5bd..26a1ab4b15 100644 --- a/app/component/ItineraryTab.js +++ b/app/component/ItineraryTab.js @@ -1,4 +1,3 @@ -import get from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; import { createFragmentContainer, graphql } from 'react-relay'; @@ -6,8 +5,7 @@ import cx from 'classnames'; import { matchShape, routerShape } from 'found'; import { FormattedMessage, intlShape } from 'react-intl'; import connectToStores from 'fluxible-addons-react/connectToStores'; - -import Icon from './Icon'; +import get from 'lodash/get'; import TicketInformation from './TicketInformation'; import RouteInformation from './RouteInformation'; import ItinerarySummary from './ItinerarySummary'; @@ -15,17 +13,17 @@ import ItineraryLegs from './ItineraryLegs'; import BackButton from './BackButton'; import MobileTicketPurchaseInformation from './MobileTicketPurchaseInformation'; import { - getRoutes, - getZones, compressLegs, + getRoutes, getTotalBikingDistance, getTotalBikingDuration, + getTotalDrivingDistance, + getTotalDrivingDuration, getTotalWalkingDistance, getTotalWalkingDuration, - legContainsRentalBike, - getTotalDrivingDuration, - getTotalDrivingDistance, + getZones, isCallAgencyPickupType, + legContainsRentalBike, } from '../util/legUtils'; import { BreakpointConsumer } from '../util/withBreakpoint'; @@ -36,14 +34,15 @@ import { } from '../util/fareUtils'; import { addAnalyticsEvent } from '../util/analyticsUtils'; import { + getCurrentMillis, + getFormattedTimeDate, isToday, isTomorrow, - getFormattedTimeDate, - getCurrentMillis, } from '../util/timeUtils'; import CityBikeDurationInfo from './CityBikeDurationInfo'; import { getCityBikeNetworkId } from '../util/citybikes'; import { FareShape } from '../util/shapes'; +import FareDisclaimer from './FareDisclaimer'; const AlertShape = PropTypes.shape({ alertSeverityLevel: PropTypes.string }); @@ -87,7 +86,7 @@ class ItineraryTab extends React.Component { static defaultProps = { hideTitle: false, - currentLanguage: "fi" + currentLanguage: 'fi', }; static contextTypes = { @@ -159,7 +158,7 @@ class ItineraryTab extends React.Component { walkingDistance > 0 && (bikingDistance > 0 || drivingDistance > 0) && futureText !== ''; - const extraProps = { + return { walking: { duration: walkingDuration, distance: walkingDistance, @@ -175,7 +174,6 @@ class ItineraryTab extends React.Component { futureText, isMultiRow, }; - return extraProps; }; render() { @@ -217,8 +215,60 @@ class ItineraryTab extends React.Component { const suggestionIndex = this.context.match.params.secondHash ? Number(this.context.match.params.secondHash) + 1 : Number(this.context.match.params.hash) + 1; - const itineraryContainsCallLegs = itinerary.legs.some(leg => isCallAgencyPickupType(leg)); - + + const disclaimers = []; + + if (shouldShowFareInfo(config) && fares.some(fare => fare.isUnknown)) { + const found = {}; + itinerary.legs.forEach(leg => { + if ( + config.modeDisclaimers?.[leg.mode] && + !found[leg.mode] + ) { + found[leg.mode] = true; + const disclaimer = config.modeDisclaimers[leg.mode][currentLanguage]; + disclaimers.push( + , + ); + } + }); + + if (config.callAgencyInfo && itinerary.legs.some(leg => isCallAgencyPickupType(leg))) { + disclaimers.push( + , + ); + } + + if (!disclaimers.length) { + disclaimers.push( + , + ); + } + } + return (

@@ -241,7 +291,11 @@ class ItineraryTab extends React.Component { futureText={extraProps.futureText} isMultiRow={extraProps.isMultiRow} isMobile={this.props.isMobile} - hideBottomDivider={shouldShowFarePurchaseInfo(config, breakpoint, fares)} + hideBottomDivider={shouldShowFarePurchaseInfo( + config, + breakpoint, + fares, + )} /> ) : ( <> @@ -282,18 +336,19 @@ class ItineraryTab extends React.Component { config={config} /> ), - shouldShowFareInfo(config) && ( - shouldShowFarePurchaseInfo(config,breakpoint,fares) ? ( + shouldShowFareInfo(config) && + (shouldShowFarePurchaseInfo(config, breakpoint, fares) ? ( ) : - ( + ) : ( + ) - ), + /> + )),
- {shouldShowFareInfo(config) && - fares.some(fare => fare.isUnknown) && ( -
-
- -
- {config.callAgencyInfo && itineraryContainsCallLegs ? - (
- - - - -
- ) : ( -
- -
- )} -
- )} + <>{disclaimers} , ]; - const LegRouteName = leg.from.name.concat(' - ').concat(leg.to.name); const modeClassName = mode.toLowerCase(); + const LegRouteName = leg.from.name.concat(' - ').concat(leg.to.name); const textVersionBeforeLink = ( )}
- {leg.fare && leg.fare.isUnknown && shouldShowFareInfo(config) && ( -
-
- - {`${intl.formatMessage({ id: 'pay-attention' })} `} - - {intl.formatMessage({ id: 'separate-ticket-required' })} + {leg.fare?.isUnknown && + shouldShowFareInfo(config) && + (config.modeDisclaimers && config.modeDisclaimers[mode] ? ( +
+
+ + + + +
-
-
{LegRouteName}
- {leg.fare.agency && - !config.hideExternalOperator(leg.fare.agency) && ( - -
{leg.fare.agency.name}
- {leg.fare.agency.fareUrl && ( - - {intl.formatMessage({ id: 'extra-info' })} - - )} -
- )} + ) : ( +
+
+ + {`${intl.formatMessage({ id: 'pay-attention' })} `} + + {intl.formatMessage({ id: 'separate-ticket-required' })} +
+
+
{LegRouteName}
+ {leg.fare.agency && + !config.hideExternalOperator(leg.fare.agency) && ( + +
{leg.fare.agency.name}
+ {leg.fare.agency.fareUrl && ( + + {intl.formatMessage({ id: 'extra-info' })} + + )} +
+ )} +
-
- )} + ))}
- {alertSeverityDescription} + {alertSeverityDescription};
); }; diff --git a/app/configurations/config.tampere.js b/app/configurations/config.tampere.js index 92c6f74da2..bc8f37edf3 100644 --- a/app/configurations/config.tampere.js +++ b/app/configurations/config.tampere.js @@ -95,6 +95,29 @@ export default configMerger(walttiConfig, { }, }, + modeDisclaimers: { + RAIL: { + fi: { + disclaimer: + 'Nyssen liput käyvät junaliikenteessä rajoitetusti vain Nysse-alueella. Lue lisää ', + link: 'https://www.nysse.fi/junat', + text: 'nysse.fi/junat', + }, + sv: { + disclaimer: + 'Nysse-biljetter är giltiga på tåg i Nysse-området, med vissa begränsningar. Läs mer på ', + link: 'https://www.nysse.fi/en/ways-to-get-around/train', + text: 'Trains in the Nysse area - Nysse, Tampere regional transport', + }, + en: { + disclaimer: + 'Nysse tickets are valid on trains in the Nysse area with some limitations. Read more on ', + link: 'https://www.nysse.fi/en/ways-to-get-around/train', + text: 'Trains in the Nysse area - Nysse, Tampere regional transport', + }, + }, + }, + // mapping fareId from OTP fare identifiers to human readable form fareMapping: function mapFareId(fareId) { return fareId && fareId.substring diff --git a/app/util/legUtils.js b/app/util/legUtils.js index 4d77fb4e50..b89b14edd5 100644 --- a/app/util/legUtils.js +++ b/app/util/legUtils.js @@ -17,23 +17,6 @@ function filterLegStops(leg, filter) { return false; } -/** - * Check if legs start stop pickuptype or end stop pickupType is CALL_AGENCY - * - * leg must have: - * from.stop.gtfsId - * to.stop.gtfsId - * trip.stoptimes (with props:) - * stop.gtfsId - * pickupType - */ -export function isCallAgencyPickupType(leg) { - return ( - filterLegStops(leg, stoptime => stoptime.pickupType === 'CALL_AGENCY') - .length > 0 - ); -} - export function isCallAgencyDeparture(departure) { return departure.pickupType === 'CALL_AGENCY'; } @@ -70,6 +53,7 @@ export const LegMode = { CityBike: 'CITYBIKE', Walk: 'WALK', Car: 'CAR', + Rail: 'RAIL', }; /** @@ -94,11 +78,30 @@ export const getLegMode = legOrMode => { return LegMode.Walk; case LegMode.Car: return LegMode.Car; + case LegMode.Rail: + return LegMode.Rail; default: return undefined; } }; +/** + * Check if legs start stop pickuptype or end stop pickupType is CALL_AGENCY + * + * leg must have: + * from.stop.gtfsId + * to.stop.gtfsId + * trip.stoptimes (with props:) + * stop.gtfsId + * pickupType + */ +export function isCallAgencyPickupType(leg) { + return ( + filterLegStops(leg, stoptime => stoptime.pickupType === 'CALL_AGENCY') + .length > 0 + ); +} + /** * Checks if both of the legs exist and are taken with mode 'BICYCLE'. *