@duffel/components 2.7.19 → 3.0.0-canary
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +67 -0
- package/.eslintrc.js +34 -0
- package/.github/renovate.json +16 -0
- package/.github/workflows/autoapprove.yml +18 -0
- package/.github/workflows/release.yml +86 -0
- package/.husky/post-commit +4 -0
- package/.husky/pre-commit +4 -0
- package/.nvmrc +1 -0
- package/.prettierignore +2 -0
- package/.storybook/Storyshots.test.js +3 -0
- package/.storybook/__snapshots__/Storyshots.test.js.snap +48133 -0
- package/.storybook/main.ts +33 -0
- package/.storybook/preview.tsx +28 -0
- package/.tool-versions +1 -0
- package/README.md +129 -16
- package/__mocks__/styleMock.js +6 -0
- package/babel.config.js +20 -0
- package/commitlint.config.js +4 -0
- package/config/esbuild.base.config.js +14 -0
- package/config/esbuild.cdn.config.js +50 -0
- package/config/esbuild.dev.config.js +45 -0
- package/config/esbuild.react.config.js +42 -0
- package/jest.config.ts +14 -0
- package/package.json +123 -192
- package/react-dist/components/AnimatedLoaderEllipsis.d.ts +2 -0
- package/react-dist/components/Button.d.ts +23 -0
- package/react-dist/components/Card.d.ts +14 -0
- package/react-dist/components/Counter.d.ts +10 -0
- package/react-dist/components/DuffelAncillaries.d.ts +3 -0
- package/react-dist/components/DuffelAncillariesCustomElement.d.ts +13 -0
- package/react-dist/components/ErrorBoundary.d.ts +13 -0
- package/react-dist/components/FetchOfferErrorState.d.ts +5 -0
- package/react-dist/components/Icon.d.ts +44 -0
- package/react-dist/components/IconButton.d.ts +16 -0
- package/react-dist/components/Modal.d.ts +11 -0
- package/react-dist/components/NonIdealState.d.ts +4 -0
- package/react-dist/components/Stamp.d.ts +7 -0
- package/react-dist/components/Tabs.d.ts +16 -0
- package/react-dist/components/bags/BaggageSelectionCard.d.ts +11 -0
- package/react-dist/components/bags/BaggageSelectionController.d.ts +13 -0
- package/react-dist/components/bags/BaggageSelectionModal.d.ts +11 -0
- package/react-dist/components/bags/BaggageSelectionModalBody.d.ts +11 -0
- package/react-dist/components/bags/BaggageSelectionModalBodyPassenger.d.ts +13 -0
- package/react-dist/components/bags/BaggageSelectionModalFooter.d.ts +14 -0
- package/react-dist/components/bags/BaggageSelectionModalHeader.d.ts +9 -0
- package/react-dist/components/bags/IncludedBaggageBanner.d.ts +7 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionCard.d.ts +10 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionModal.d.ts +11 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBody.d.ts +7 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.d.ts +4 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionModalFooter.d.ts +11 -0
- package/react-dist/components/cancel_for_any_reason/CfarSelectionModalHeader.d.ts +2 -0
- package/react-dist/components/seats/Amenity.d.ts +6 -0
- package/react-dist/components/seats/DeckSelect.d.ts +15 -0
- package/react-dist/components/seats/Element.d.ts +15 -0
- package/react-dist/components/seats/EmptyElement.d.ts +2 -0
- package/react-dist/components/seats/ExitElement.d.ts +6 -0
- package/react-dist/components/seats/Legend.d.ts +12 -0
- package/react-dist/components/seats/Row.d.ts +13 -0
- package/react-dist/components/seats/RowSection.d.ts +17 -0
- package/react-dist/components/seats/SeatElement.d.ts +13 -0
- package/react-dist/components/seats/SeatInfo.d.ts +7 -0
- package/react-dist/components/seats/SeatMap.d.ts +12 -0
- package/react-dist/components/seats/SeatMapUnavailable.d.ts +2 -0
- package/react-dist/components/seats/SeatSelectionCard.d.ts +13 -0
- package/react-dist/components/seats/SeatSelectionModal.d.ts +13 -0
- package/react-dist/components/seats/SeatSelectionModalBody.d.ts +4 -0
- package/react-dist/components/seats/SeatSelectionModalFooter.d.ts +16 -0
- package/react-dist/components/seats/SeatSelectionModalHeader.d.ts +10 -0
- package/react-dist/components/seats/SeatUnavailable.d.ts +5 -0
- package/react-dist/index.d.ts +8 -0
- package/react-dist/index.js +36 -0
- package/react-dist/index.js.map +7 -0
- package/react-dist/lib/captureErrorInSentry.d.ts +1 -0
- package/react-dist/lib/compileCreateOrderPayload.d.ts +14 -0
- package/react-dist/lib/createPriceFormatters.d.ts +12 -0
- package/react-dist/lib/fetchFromDuffelAPI.d.ts +1 -0
- package/react-dist/lib/fetchFromFixtures.d.ts +4 -0
- package/react-dist/lib/formatAvailableServices.d.ts +12 -0
- package/react-dist/lib/formatDate.d.ts +2 -0
- package/react-dist/lib/formatSeatMaps.d.ts +4 -0
- package/react-dist/lib/getBaggageServiceDescription.d.ts +2 -0
- package/react-dist/lib/getCabinsForSegmentAndDeck.d.ts +2 -0
- package/react-dist/lib/getCurrencyForSeatMaps.d.ts +10 -0
- package/react-dist/lib/getCurrencyForServices.d.ts +11 -0
- package/react-dist/lib/getFirstSeatElementMatchingCriteria.d.ts +3 -0
- package/react-dist/lib/getPassengerBySegmentList.d.ts +6 -0
- package/react-dist/lib/getPassengerInitials.d.ts +1 -0
- package/react-dist/lib/getPassengerMapById.d.ts +3 -0
- package/react-dist/lib/getPassengerName.d.ts +3 -0
- package/react-dist/lib/getRowNumber.d.ts +2 -0
- package/react-dist/lib/getSegmentList.d.ts +2 -0
- package/react-dist/lib/getServicePriceMapById.d.ts +3 -0
- package/react-dist/lib/getSymbols.d.ts +2 -0
- package/react-dist/lib/getTotalAmountForServices.d.ts +6 -0
- package/react-dist/lib/getTotalQuantity.d.ts +2 -0
- package/react-dist/lib/hasService.d.ts +2 -0
- package/react-dist/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.d.ts +3 -0
- package/react-dist/lib/hasWings.d.ts +2 -0
- package/react-dist/lib/isBaggageService.d.ts +2 -0
- package/react-dist/lib/isCancelForAnyReasonService.d.ts +2 -0
- package/react-dist/lib/isFixtureOfferId.d.ts +2 -0
- package/react-dist/lib/isPayloadComplete.d.ts +2 -0
- package/react-dist/lib/isSeatElement.d.ts +2 -0
- package/react-dist/lib/logging.d.ts +53 -0
- package/react-dist/lib/moneyStringFormatter.d.ts +8 -0
- package/react-dist/lib/offerIsExpired.d.ts +2 -0
- package/react-dist/lib/retrieveOffer.d.ts +2 -0
- package/react-dist/lib/retrieveOfferFromDuffelAPI.d.ts +1 -0
- package/react-dist/lib/retrieveSeatMaps.d.ts +2 -0
- package/react-dist/lib/retrieveSeatMapsFromDuffelAPI.d.ts +1 -0
- package/react-dist/lib/setBodyScrollability.d.ts +1 -0
- package/react-dist/lib/validateProps.d.ts +7 -0
- package/react-dist/lib/withPlural.d.ts +1 -0
- package/react-dist/types/Aircraft.d.ts +14 -0
- package/react-dist/types/Airline.d.ts +14 -0
- package/react-dist/types/Airport.d.ts +44 -0
- package/react-dist/types/City.d.ts +18 -0
- package/react-dist/types/CreateOrderPayload.d.ts +72 -0
- package/react-dist/types/CurrencyConversion.d.ts +10 -0
- package/react-dist/types/DuffelAncillariesProps.d.ts +70 -0
- package/react-dist/types/Offer.d.ts +711 -0
- package/react-dist/types/Order.d.ts +8 -0
- package/react-dist/types/Place.d.ts +8 -0
- package/react-dist/types/SeatMap.d.ts +190 -0
- package/react-dist/types/index.d.ts +11 -0
- package/scripts/generate-fixture.ts +195 -0
- package/scripts/upload-to-cdn.sh +39 -0
- package/scripts.tsconfig.json +11 -0
- package/src/components/AnimatedLoaderEllipsis.tsx +5 -0
- package/src/components/Button.tsx +62 -0
- package/src/components/Card.tsx +126 -0
- package/src/components/Counter.tsx +40 -0
- package/src/components/DuffelAncillaries.tsx +346 -0
- package/src/components/DuffelAncillariesCustomElement.tsx +124 -0
- package/src/components/ErrorBoundary.tsx +54 -0
- package/src/components/FetchOfferErrorState.tsx +35 -0
- package/src/components/Icon.tsx +151 -0
- package/src/components/IconButton.tsx +42 -0
- package/src/components/Modal.tsx +36 -0
- package/src/components/NonIdealState.tsx +28 -0
- package/src/components/Stamp.tsx +29 -0
- package/src/components/Tabs.tsx +36 -0
- package/src/components/bags/BaggageSelectionCard.tsx +96 -0
- package/src/components/bags/BaggageSelectionController.tsx +88 -0
- package/src/components/bags/BaggageSelectionModal.tsx +81 -0
- package/src/components/bags/BaggageSelectionModalBody.tsx +60 -0
- package/src/components/bags/BaggageSelectionModalBodyPassenger.tsx +122 -0
- package/src/components/bags/BaggageSelectionModalFooter.tsx +81 -0
- package/src/components/bags/BaggageSelectionModalHeader.tsx +76 -0
- package/src/components/bags/IncludedBaggageBanner.tsx +51 -0
- package/src/components/cancel_for_any_reason/CfarSelectionCard.tsx +90 -0
- package/src/components/cancel_for_any_reason/CfarSelectionModal.tsx +63 -0
- package/src/components/cancel_for_any_reason/CfarSelectionModalBody.tsx +56 -0
- package/src/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.tsx +11 -0
- package/src/components/cancel_for_any_reason/CfarSelectionModalFooter.tsx +74 -0
- package/src/components/cancel_for_any_reason/CfarSelectionModalHeader.tsx +9 -0
- package/src/components/seats/Amenity.tsx +21 -0
- package/src/components/seats/DeckSelect.tsx +27 -0
- package/src/components/seats/Element.tsx +52 -0
- package/src/components/seats/EmptyElement.tsx +5 -0
- package/src/components/seats/ExitElement.tsx +17 -0
- package/src/components/seats/Legend.tsx +60 -0
- package/src/components/seats/Row.tsx +47 -0
- package/src/components/seats/RowSection.tsx +75 -0
- package/src/components/seats/SeatElement.tsx +120 -0
- package/src/components/seats/SeatInfo.tsx +32 -0
- package/src/components/seats/SeatMap.tsx +81 -0
- package/src/components/seats/SeatMapUnavailable.tsx +21 -0
- package/src/components/seats/SeatSelectionCard.tsx +103 -0
- package/src/components/seats/SeatSelectionModal.tsx +142 -0
- package/src/components/seats/SeatSelectionModalBody.tsx +13 -0
- package/src/components/seats/SeatSelectionModalFooter.tsx +82 -0
- package/src/components/seats/SeatSelectionModalHeader.tsx +87 -0
- package/src/components/seats/SeatUnavailable.tsx +14 -0
- package/src/examples/client-side/index.html +57 -0
- package/src/examples/full-stack/index.html +48 -0
- package/src/examples/full-stack/server.mjs +157 -0
- package/src/examples/just-typescript/README.md +37 -0
- package/src/examples/just-typescript/package.json +16 -0
- package/src/examples/just-typescript/src/index.html +23 -0
- package/src/examples/just-typescript/src/index.ts +35 -0
- package/src/examples/just-typescript/yarn.lock +154 -0
- package/src/examples/react-app/README.md +37 -0
- package/src/examples/react-app/package.json +20 -0
- package/src/examples/react-app/src/index.html +19 -0
- package/src/examples/react-app/src/index.tsx +43 -0
- package/src/examples/react-app/yarn.lock +219 -0
- package/src/fixtures/offers/off_0000AUde3KwTztSRK1cznH.json +497 -0
- package/src/fixtures/offers/off_0000AVx4lUFFKW8PsPeQeQ.json +307 -0
- package/src/fixtures/offers/off_1.json +497 -0
- package/src/fixtures/passengers/mock_passengers.ts +26 -0
- package/src/fixtures/seat-maps/off_0000AUde3KwTztSRK1cznH.json +6852 -0
- package/src/fixtures/seat-maps/off_0000AVx4lUFFKW8PsPeQeQ.json +1 -0
- package/src/fixtures/seat-maps/off_1.json +6852 -0
- package/src/index.ts +8 -0
- package/src/lib/captureErrorInSentry.ts +60 -0
- package/src/lib/compileCreateOrderPayload.ts +63 -0
- package/src/lib/createPriceFormatters.ts +73 -0
- package/src/lib/fetchFromDuffelAPI.ts +24 -0
- package/src/lib/fetchFromFixtures.ts +18 -0
- package/src/lib/formatAvailableServices.ts +91 -0
- package/src/lib/formatDate.ts +21 -0
- package/src/lib/formatSeatMaps.ts +81 -0
- package/src/lib/getBaggageServiceDescription.ts +44 -0
- package/src/lib/getCabinsForSegmentAndDeck.ts +4 -0
- package/src/lib/getCurrencyForSeatMaps.ts +22 -0
- package/src/lib/getCurrencyForServices.ts +24 -0
- package/src/lib/getFirstSeatElementMatchingCriteria.ts +22 -0
- package/src/lib/getPassengerBySegmentList.ts +10 -0
- package/src/lib/getPassengerInitials.ts +6 -0
- package/src/lib/getPassengerMapById.ts +17 -0
- package/src/lib/getPassengerName.ts +37 -0
- package/src/lib/getRowNumber.ts +16 -0
- package/src/lib/getSegmentList.ts +7 -0
- package/src/lib/getServicePriceMapById.ts +20 -0
- package/src/lib/getSymbols.ts +22 -0
- package/src/lib/getTotalAmountForServices.ts +72 -0
- package/src/lib/getTotalQuantity.ts +5 -0
- package/src/lib/hasService.ts +24 -0
- package/src/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.ts +35 -0
- package/src/lib/hasWings.ts +8 -0
- package/src/lib/isBaggageService.ts +8 -0
- package/src/lib/isCancelForAnyReasonService.ts +9 -0
- package/src/lib/isFixtureOfferId.ts +4 -0
- package/src/lib/isPayloadComplete.ts +11 -0
- package/src/lib/isSeatElement.ts +10 -0
- package/src/lib/logging.ts +100 -0
- package/src/lib/moneyStringFormatter.ts +34 -0
- package/src/lib/offerIsExpired.ts +5 -0
- package/src/lib/retrieveOffer.ts +49 -0
- package/src/lib/retrieveOfferFromDuffelAPI.ts +13 -0
- package/src/lib/retrieveSeatMaps.ts +50 -0
- package/src/lib/retrieveSeatMapsFromDuffelAPI.ts +13 -0
- package/src/lib/setBodyScrollability.ts +7 -0
- package/src/lib/validateProps.ts +37 -0
- package/src/lib/withPlural.ts +8 -0
- package/src/stories/BaggageSelectionModalHeader.stories.tsx +21 -0
- package/src/stories/Button.stories.tsx +60 -0
- package/src/stories/DuffelAncillaries.stories.tsx +126 -0
- package/src/stories/Icon.stories.tsx +34 -0
- package/src/stories/IconButton.stories.tsx +25 -0
- package/src/styles/colors.css +22 -0
- package/src/styles/components/Amenity.css +23 -0
- package/src/styles/components/BaggageDisplay.css +25 -0
- package/src/styles/components/Button.css +161 -0
- package/src/styles/components/Card.css +52 -0
- package/src/styles/components/CfarSelectionModal.css +34 -0
- package/src/styles/components/Counter.css +18 -0
- package/src/styles/components/IconButton.css +63 -0
- package/src/styles/components/Legend.css +58 -0
- package/src/styles/components/Loader.css +37 -0
- package/src/styles/components/LoadingState.css +81 -0
- package/src/styles/components/Modal.css +83 -0
- package/src/styles/components/PassengerSelect.css +93 -0
- package/src/styles/components/PassengersLayout.css +90 -0
- package/src/styles/components/Row.css +70 -0
- package/src/styles/components/Seat.css +57 -0
- package/src/styles/components/SeatInfo.css +61 -0
- package/src/styles/components/SeatMap.css +24 -0
- package/src/styles/components/SeatSelect.css +92 -0
- package/src/styles/components/Segment.css +17 -0
- package/src/styles/components/SelectionSegment.css +10 -0
- package/src/styles/components/Summary.css +70 -0
- package/src/styles/components/Tabs.css +49 -0
- package/src/styles/flex.css +5 -0
- package/src/styles/font-families.css +47 -0
- package/src/styles/global.css +50 -0
- package/src/styles/margin.css +3 -0
- package/src/styles/spacing.css +18 -0
- package/src/styles/transitions.css +3 -0
- package/src/styles/typography.css +13 -0
- package/src/tests/components/DuffelAncillaries.test.tsx +342 -0
- package/src/tests/lib/createPriceFormatters.test.tsx +152 -0
- package/src/tests/lib/formatAvailableServices.test.tsx +79 -0
- package/src/tests/lib/formatSeatMaps.test.tsx +49 -0
- package/src/tests/lib/getCurrencyForServices.test.tsx +44 -0
- package/src/tests/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.test.ts +86 -0
- package/src/tests/lib/logging.test.tsx +32 -0
- package/src/tests/lib/moneyStringFormatter.test.tsx +12 -0
- package/src/tests/lib/validateProps.test.tsx +57 -0
- package/src/types/Aircraft.ts +16 -0
- package/src/types/Airline.ts +16 -0
- package/src/types/Airport.ts +54 -0
- package/src/types/City.ts +21 -0
- package/src/types/CreateOrderPayload.ts +99 -0
- package/src/types/CurrencyConversion.ts +10 -0
- package/src/types/DuffelAncillariesProps.ts +108 -0
- package/src/types/Offer.ts +851 -0
- package/src/types/Order.ts +6 -0
- package/src/types/Place.ts +6 -0
- package/src/types/SeatMap.ts +231 -0
- package/src/types/index.ts +11 -0
- package/tsconfig.json +52 -0
- package/LICENSE +0 -21
- package/dist/AdditionalBaggage.esm.js +0 -1
- package/dist/AdditionalBaggage.js +0 -1
- package/dist/AdditionalBaggage.min.css +0 -408
- package/dist/AdditionalBaggage.umd.min.js +0 -2
- package/dist/AdditionalBaggage.umd.min.js.LICENSE.txt +0 -60
- package/dist/AdditionalBaggageSelection.esm.js +0 -1
- package/dist/AdditionalBaggageSelection.js +0 -1
- package/dist/AdditionalBaggageSelection.min.css +0 -744
- package/dist/AdditionalBaggageSelection.umd.min.js +0 -2
- package/dist/AdditionalBaggageSelection.umd.min.js.LICENSE.txt +0 -93
- package/dist/CardPayment.esm.js +0 -2
- package/dist/CardPayment.esm.js.LICENSE.txt +0 -6
- package/dist/CardPayment.js +0 -2
- package/dist/CardPayment.js.LICENSE.txt +0 -6
- package/dist/CardPayment.min.css +0 -233
- package/dist/CardPayment.umd.min.js +0 -2
- package/dist/CardPayment.umd.min.js.LICENSE.txt +0 -61
- package/dist/SeatSelection.esm.js +0 -1
- package/dist/SeatSelection.js +0 -1
- package/dist/SeatSelection.min.css +0 -1127
- package/dist/SeatSelection.umd.min.js +0 -2
- package/dist/SeatSelection.umd.min.js.LICENSE.txt +0 -60
- package/dist/duffel-components.d.ts +0 -1614
- package/dist/duffel-components.esm.js +0 -2
- package/dist/duffel-components.esm.js.LICENSE.txt +0 -6
- package/dist/duffel-components.js +0 -2
- package/dist/duffel-components.js.LICENSE.txt +0 -6
- package/dist/duffel-components.min.css +0 -1280
- package/dist/duffel-components.umd.min.js +0 -2
- package/dist/duffel-components.umd.min.js.LICENSE.txt +0 -102
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { IconButton } from "./IconButton";
|
|
3
|
+
|
|
4
|
+
interface CounterProps {
|
|
5
|
+
id: string;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
value: number;
|
|
9
|
+
onChange: (value: number) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Counter: React.FC<CounterProps> = ({
|
|
13
|
+
id,
|
|
14
|
+
min,
|
|
15
|
+
max,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
}) => (
|
|
19
|
+
<div className="counter" id={id}>
|
|
20
|
+
<IconButton
|
|
21
|
+
icon="minus"
|
|
22
|
+
title="Remove one"
|
|
23
|
+
id={`${id}-minus`}
|
|
24
|
+
data-testid={`${id}-minus`}
|
|
25
|
+
variant="outlined"
|
|
26
|
+
disabled={value <= min}
|
|
27
|
+
onClick={() => onChange(Math.max(value - 1, min))}
|
|
28
|
+
/>
|
|
29
|
+
<div className="counter__count-label">{value}</div>
|
|
30
|
+
<IconButton
|
|
31
|
+
icon="add"
|
|
32
|
+
title="Add one"
|
|
33
|
+
id={`${id}-plus`}
|
|
34
|
+
data-testid={`${id}-plus`}
|
|
35
|
+
variant="outlined"
|
|
36
|
+
disabled={value >= max}
|
|
37
|
+
onClick={() => onChange(Math.min(value + 1, max))}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { compileCreateOrderPayload } from "@lib/compileCreateOrderPayload";
|
|
2
|
+
import { createPriceFormatters } from "@lib/createPriceFormatters";
|
|
3
|
+
import { formatAvailableServices } from "@lib/formatAvailableServices";
|
|
4
|
+
import { formatSeatMaps } from "@lib/formatSeatMaps";
|
|
5
|
+
import { isPayloadComplete } from "@lib/isPayloadComplete";
|
|
6
|
+
import { LogContext, initializeLogger } from "@lib/logging";
|
|
7
|
+
import { offerIsExpired } from "@lib/offerIsExpired";
|
|
8
|
+
import { retrieveOffer } from "@lib/retrieveOffer";
|
|
9
|
+
import { retrieveSeatMaps } from "@lib/retrieveSeatMaps";
|
|
10
|
+
import {
|
|
11
|
+
areDuffelAncillariesPropsValid,
|
|
12
|
+
isDuffelAncillariesPropsWithClientKeyAndOfferId,
|
|
13
|
+
isDuffelAncillariesPropsWithOfferAndClientKey,
|
|
14
|
+
isDuffelAncillariesPropsWithOfferAndSeatMaps,
|
|
15
|
+
isDuffelAncillariesPropsWithOfferIdForFixture,
|
|
16
|
+
} from "@lib/validateProps";
|
|
17
|
+
import * as Sentry from "@sentry/browser";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import {
|
|
20
|
+
CreateOrderPayloadPassengers,
|
|
21
|
+
CreateOrderPayloadService,
|
|
22
|
+
} from "../types/CreateOrderPayload";
|
|
23
|
+
import { DuffelAncillariesProps } from "../types/DuffelAncillariesProps";
|
|
24
|
+
import { Offer } from "../types/Offer";
|
|
25
|
+
import { SeatMap } from "../types/SeatMap";
|
|
26
|
+
import { ErrorBoundary } from "./ErrorBoundary";
|
|
27
|
+
import { FetchOfferErrorState } from "./FetchOfferErrorState";
|
|
28
|
+
import { BaggageSelectionCard } from "./bags/BaggageSelectionCard";
|
|
29
|
+
import { CfarSelectionCard } from "./cancel_for_any_reason/CfarSelectionCard";
|
|
30
|
+
import { SeatSelectionCard } from "./seats/SeatSelectionCard";
|
|
31
|
+
|
|
32
|
+
const COMPONENT_CDN = process.env.COMPONENT_CDN || "";
|
|
33
|
+
const hrefToComponentStyles = `${COMPONENT_CDN}/global.css`;
|
|
34
|
+
|
|
35
|
+
export const DuffelAncillaries: React.FC<DuffelAncillariesProps> = (props) => {
|
|
36
|
+
const logger = initializeLogger(props.debug || false);
|
|
37
|
+
|
|
38
|
+
logger.logGroup("Properties passed into the component:", props);
|
|
39
|
+
|
|
40
|
+
if (!areDuffelAncillariesPropsValid(props)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`The props (${Object.keys(
|
|
43
|
+
props
|
|
44
|
+
)}) passed to DuffelAncillaries are invalid. ` +
|
|
45
|
+
"`onPayloadReady`, `passengers` and `services` are always required. " +
|
|
46
|
+
"Then, depending on your use case you may have one of the following combinations of required props: " +
|
|
47
|
+
"`offer_id` and `client_key`, `offer` and `seat_maps` or `offer` and `client_key`." +
|
|
48
|
+
"Please refer to the documentation for more information and working examples: " +
|
|
49
|
+
"https://duffel.com/docs/guides/ancillaries-component"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (props.services.length === 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`You must provide at least one service in the "services" prop. Valid services: ["bags", "seats", "cancel_for_any_reason"]`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isPropsWithOfferIdForFixture =
|
|
59
|
+
isDuffelAncillariesPropsWithOfferIdForFixture(props);
|
|
60
|
+
|
|
61
|
+
const isPropsWithClientKeyAndOfferId =
|
|
62
|
+
isDuffelAncillariesPropsWithClientKeyAndOfferId(props);
|
|
63
|
+
|
|
64
|
+
const isPropsWithOfferAndSeatMaps =
|
|
65
|
+
isDuffelAncillariesPropsWithOfferAndSeatMaps(props);
|
|
66
|
+
|
|
67
|
+
const isPropsWithOfferAndClientKey =
|
|
68
|
+
isDuffelAncillariesPropsWithOfferAndClientKey(props);
|
|
69
|
+
|
|
70
|
+
const shouldRetrieveSeatMaps =
|
|
71
|
+
props.services.includes("seats") &&
|
|
72
|
+
!("seat_maps" in props) &&
|
|
73
|
+
(isPropsWithOfferIdForFixture ||
|
|
74
|
+
isPropsWithClientKeyAndOfferId ||
|
|
75
|
+
isPropsWithOfferAndClientKey);
|
|
76
|
+
|
|
77
|
+
const [passengers, setPassengers] =
|
|
78
|
+
React.useState<CreateOrderPayloadPassengers>(props.passengers);
|
|
79
|
+
|
|
80
|
+
const [offer, setOffer] = React.useState<Offer | undefined>(
|
|
81
|
+
(props as any).offer
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const [isOfferLoading, setIsOfferLoading] = React.useState(
|
|
85
|
+
isPropsWithClientKeyAndOfferId
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const [seatMaps, setSeatMaps] = React.useState<SeatMap[] | undefined>(
|
|
89
|
+
isPropsWithOfferAndSeatMaps ? props.seat_maps : undefined
|
|
90
|
+
);
|
|
91
|
+
const [isSeatMapLoading, setIsSeatMapLoading] = React.useState(
|
|
92
|
+
shouldRetrieveSeatMaps
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const [error, setError] = React.useState<null | string>(null);
|
|
96
|
+
|
|
97
|
+
const [baggageSelectedServices, setBaggageSelectedServices] = React.useState(
|
|
98
|
+
new Array<CreateOrderPayloadService>()
|
|
99
|
+
);
|
|
100
|
+
const [seatSelectedServices, setSeatSelectedServices] = React.useState(
|
|
101
|
+
new Array<CreateOrderPayloadService>()
|
|
102
|
+
);
|
|
103
|
+
const [cfarSelectedServices, setCfarSelectedServices] = React.useState(
|
|
104
|
+
new Array<CreateOrderPayloadService>()
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const priceFormatters = createPriceFormatters(
|
|
108
|
+
props.markup,
|
|
109
|
+
props.priceFormatters
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const updateOffer = (offer: Offer) => {
|
|
113
|
+
const expiryErrorMessage = "This offer has expired.";
|
|
114
|
+
if (offerIsExpired(offer)) {
|
|
115
|
+
setError(expiryErrorMessage);
|
|
116
|
+
return;
|
|
117
|
+
} else {
|
|
118
|
+
const msUntilExpiry = new Date(offer.expires_at).getTime() - Date.now();
|
|
119
|
+
|
|
120
|
+
// Only show the expiry error message if the offer expires in less than a day,
|
|
121
|
+
// to prevent buffer overflows when showing offers for fixtures, which expire in
|
|
122
|
+
// years.
|
|
123
|
+
const milisecondsInOneDay = 1000 * 60 * 60 * 24;
|
|
124
|
+
if (msUntilExpiry < milisecondsInOneDay) {
|
|
125
|
+
setTimeout(() => setError(expiryErrorMessage), msUntilExpiry);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const offerWithFormattedServices = formatAvailableServices(
|
|
130
|
+
offer,
|
|
131
|
+
priceFormatters
|
|
132
|
+
);
|
|
133
|
+
setOffer(offerWithFormattedServices);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const updateSeatMaps = (seatMaps: SeatMap[]) => {
|
|
137
|
+
const formattedSeatMaps = formatSeatMaps(seatMaps, priceFormatters.seats);
|
|
138
|
+
setSeatMaps(formattedSeatMaps);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
// whenever the props change, we'll set the sentry context to thse values
|
|
143
|
+
// so that we can see them in the sentry logs and better support the users of the component library
|
|
144
|
+
Sentry.setContext("props", {
|
|
145
|
+
"props.services": props.services,
|
|
146
|
+
"props.passengers.length": (props as any).passengers.length,
|
|
147
|
+
"props.offer_id": (props as any).offer_id,
|
|
148
|
+
"props.client_key": (props as any).client_key,
|
|
149
|
+
"props.offer?.id": (props as any).offer?.id,
|
|
150
|
+
"props.seat_maps?.[0]?.id": (props as any).seat_maps?.[0]?.id,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (isPropsWithClientKeyAndOfferId || isPropsWithOfferIdForFixture) {
|
|
154
|
+
retrieveOffer(
|
|
155
|
+
props.offer_id,
|
|
156
|
+
!isPropsWithOfferIdForFixture ? props.client_key : null,
|
|
157
|
+
setError,
|
|
158
|
+
setIsOfferLoading,
|
|
159
|
+
(offer) => {
|
|
160
|
+
updateOffer(offer);
|
|
161
|
+
|
|
162
|
+
if (offer.passengers.length !== passengers.length) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`The number of passengers given to \`duffel-ancillaries\` (${props.passengers.length}) doesn't match ` +
|
|
165
|
+
`the number of passengers on the given offer (${offer.passengers.length}).`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isPropsWithOfferIdForFixture) {
|
|
170
|
+
// There's no way the component users will know the passenger IDs for the fixture offer
|
|
171
|
+
// so we'll need to add them here
|
|
172
|
+
setPassengers(
|
|
173
|
+
props.passengers.map((passenger, index) => ({
|
|
174
|
+
...passenger,
|
|
175
|
+
id: offer.passengers[index].id,
|
|
176
|
+
}))
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (shouldRetrieveSeatMaps) {
|
|
184
|
+
retrieveSeatMaps(
|
|
185
|
+
isPropsWithClientKeyAndOfferId || isPropsWithOfferIdForFixture
|
|
186
|
+
? props.offer_id
|
|
187
|
+
: props.offer.id,
|
|
188
|
+
!isPropsWithOfferIdForFixture ? props.client_key : null,
|
|
189
|
+
setError,
|
|
190
|
+
setIsSeatMapLoading,
|
|
191
|
+
updateSeatMaps
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (isPropsWithOfferAndClientKey) {
|
|
196
|
+
updateOffer(props.offer);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isPropsWithOfferAndSeatMaps) {
|
|
200
|
+
updateOffer(props.offer);
|
|
201
|
+
updateSeatMaps(props.seat_maps);
|
|
202
|
+
}
|
|
203
|
+
}, [
|
|
204
|
+
// `as any` is needed here because the list
|
|
205
|
+
// of dependencies is different for each combination of props.
|
|
206
|
+
// To satisfy typescript, we'd need to conditionally assign
|
|
207
|
+
// the dependencies to the hook after checking its type,
|
|
208
|
+
// however that is not possible in a react hook.
|
|
209
|
+
(props as any).offer_id,
|
|
210
|
+
(props as any).client_key,
|
|
211
|
+
(props as any).offer?.id,
|
|
212
|
+
(props as any).seat_maps?.[0]?.id,
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
React.useEffect(() => {
|
|
216
|
+
if (!offer) return;
|
|
217
|
+
|
|
218
|
+
const createOrderPayload = compileCreateOrderPayload({
|
|
219
|
+
baggageSelectedServices,
|
|
220
|
+
seatSelectedServices,
|
|
221
|
+
cfarSelectedServices,
|
|
222
|
+
offer,
|
|
223
|
+
passengers,
|
|
224
|
+
seatMaps,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (isPayloadComplete(createOrderPayload)) {
|
|
228
|
+
const metadata = {
|
|
229
|
+
offer_total_amount: offer.total_amount,
|
|
230
|
+
offer_total_currency: offer.total_currency,
|
|
231
|
+
offer_tax_amount: offer.tax_amount,
|
|
232
|
+
offer_tax_currency: offer.tax_currency,
|
|
233
|
+
baggage_services: baggageSelectedServices,
|
|
234
|
+
seat_services: seatSelectedServices,
|
|
235
|
+
cancel_for_any_reason_services: cfarSelectedServices,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
logger.logGroup("Payload ready", {
|
|
239
|
+
"Order creation payload": createOrderPayload,
|
|
240
|
+
"Services metadata": metadata,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
props.onPayloadReady(createOrderPayload, metadata);
|
|
244
|
+
}
|
|
245
|
+
}, [baggageSelectedServices, seatSelectedServices, cfarSelectedServices]);
|
|
246
|
+
|
|
247
|
+
if (!areDuffelAncillariesPropsValid(props)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nonIdealStateHeight = `${
|
|
252
|
+
// 72 (card height) + 32 gap between cards
|
|
253
|
+
72 * props.services.length + 32 * (props.services.length - 1)
|
|
254
|
+
}px`;
|
|
255
|
+
|
|
256
|
+
const duffelComponentsStyle = {
|
|
257
|
+
// Adding inline styles here to avoid the cards jumping down
|
|
258
|
+
// before the css is loaded duet to the missing "row gap".
|
|
259
|
+
display: "flex",
|
|
260
|
+
width: "100%",
|
|
261
|
+
flexDirection: "column",
|
|
262
|
+
rowGap: "12px",
|
|
263
|
+
...(props.styles?.accentColor && {
|
|
264
|
+
"--ACCENT": props.styles.accentColor,
|
|
265
|
+
}),
|
|
266
|
+
...(props.styles?.fontFamily && {
|
|
267
|
+
"--FONT-FAMILY": props.styles.fontFamily,
|
|
268
|
+
}),
|
|
269
|
+
...(props.styles?.buttonCornerRadius && {
|
|
270
|
+
"--BUTTON-RADIUS": props.styles.buttonCornerRadius,
|
|
271
|
+
}),
|
|
272
|
+
// `as any` is needed here is needed because we want to set css variables
|
|
273
|
+
// that are not part of the css properties type
|
|
274
|
+
} as any;
|
|
275
|
+
|
|
276
|
+
const state = {
|
|
277
|
+
isOfferLoading,
|
|
278
|
+
isSeatMapLoading,
|
|
279
|
+
baggageSelectedServices,
|
|
280
|
+
seatSelectedServices,
|
|
281
|
+
cfarSelectedServices,
|
|
282
|
+
offer,
|
|
283
|
+
seatMaps,
|
|
284
|
+
error,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
logger.logGroup("Component's internal state:", state);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<>
|
|
291
|
+
<link rel="stylesheet" href={hrefToComponentStyles}></link>
|
|
292
|
+
|
|
293
|
+
<LogContext.Provider value={logger}>
|
|
294
|
+
<div className="duffel-components" style={duffelComponentsStyle}>
|
|
295
|
+
<ErrorBoundary>
|
|
296
|
+
{error && (
|
|
297
|
+
<FetchOfferErrorState
|
|
298
|
+
height={nonIdealStateHeight}
|
|
299
|
+
message={error}
|
|
300
|
+
/>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{!error &&
|
|
304
|
+
props.services.map((ancillaryName) => {
|
|
305
|
+
if (ancillaryName === "bags")
|
|
306
|
+
return (
|
|
307
|
+
<BaggageSelectionCard
|
|
308
|
+
key="bags"
|
|
309
|
+
isLoading={isOfferLoading}
|
|
310
|
+
offer={offer}
|
|
311
|
+
passengers={passengers}
|
|
312
|
+
selectedServices={baggageSelectedServices}
|
|
313
|
+
setSelectedServices={setBaggageSelectedServices}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
if (ancillaryName === "seats")
|
|
318
|
+
return (
|
|
319
|
+
<SeatSelectionCard
|
|
320
|
+
key="seats"
|
|
321
|
+
isLoading={isOfferLoading || isSeatMapLoading}
|
|
322
|
+
seatMaps={seatMaps}
|
|
323
|
+
offer={offer}
|
|
324
|
+
passengers={passengers}
|
|
325
|
+
selectedServices={seatSelectedServices}
|
|
326
|
+
setSelectedServices={setSeatSelectedServices}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (ancillaryName === "cancel_for_any_reason")
|
|
331
|
+
return (
|
|
332
|
+
<CfarSelectionCard
|
|
333
|
+
key="cancel_for_any_reason"
|
|
334
|
+
isLoading={isOfferLoading}
|
|
335
|
+
offer={offer}
|
|
336
|
+
selectedServices={cfarSelectedServices}
|
|
337
|
+
setSelectedServices={setCfarSelectedServices}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
})}
|
|
341
|
+
</ErrorBoundary>
|
|
342
|
+
</div>
|
|
343
|
+
</LogContext.Provider>
|
|
344
|
+
</>
|
|
345
|
+
);
|
|
346
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createRoot, Root } from "react-dom/client";
|
|
2
|
+
import { CreateOrderPayload } from "../types/CreateOrderPayload";
|
|
3
|
+
import {
|
|
4
|
+
DuffelAncillariesPropsWithClientKeyAndOfferId,
|
|
5
|
+
DuffelAncillariesPropsWithOfferIdForFixture,
|
|
6
|
+
DuffelAncillariesPropsWithOffersAndSeatMaps,
|
|
7
|
+
DuffelAncillariesPropWithOfferAndClientKey,
|
|
8
|
+
OnPayloadReady,
|
|
9
|
+
OnPayloadReadyMetadata,
|
|
10
|
+
} from "../types/DuffelAncillariesProps";
|
|
11
|
+
import { DuffelAncillaries } from "./DuffelAncillaries";
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
15
|
+
namespace JSX {
|
|
16
|
+
interface IntrinsicElements {
|
|
17
|
+
"duffel-ancillaries": React.DetailedHTMLProps<
|
|
18
|
+
React.HTMLAttributes<HTMLElement>,
|
|
19
|
+
HTMLElement
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CUSTOM_ELEMENT_TAG = "duffel-ancillaries";
|
|
26
|
+
|
|
27
|
+
// A bit reptitive but typescript is not clever enough
|
|
28
|
+
// to infer the correct type if we just use
|
|
29
|
+
// `Omit<DuffelAncillariesProps, 'onPayloadReady'>`
|
|
30
|
+
type DuffelAncillariesCustomElementRenderArguments =
|
|
31
|
+
| Omit<DuffelAncillariesPropsWithOfferIdForFixture, "onPayloadReady">
|
|
32
|
+
| Omit<DuffelAncillariesPropsWithClientKeyAndOfferId, "onPayloadReady">
|
|
33
|
+
| Omit<DuffelAncillariesPropWithOfferAndClientKey, "onPayloadReady">
|
|
34
|
+
| Omit<DuffelAncillariesPropsWithOffersAndSeatMaps, "onPayloadReady">;
|
|
35
|
+
|
|
36
|
+
class DuffelAncillariesCustomElement extends HTMLElement {
|
|
37
|
+
/**
|
|
38
|
+
* The React root for displaying content inside a browser DOM element.
|
|
39
|
+
*/
|
|
40
|
+
private root!: Root;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `connectedCallback` is called to initialise the custom element
|
|
44
|
+
*/
|
|
45
|
+
connectedCallback() {
|
|
46
|
+
const container = document.createElement("div");
|
|
47
|
+
this.attachShadow({ mode: "open" }).appendChild(container);
|
|
48
|
+
|
|
49
|
+
this.root = createRoot(container);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* When this function is called, it will render/re-render
|
|
54
|
+
* the `DuffelAncillaries` component with the given props.
|
|
55
|
+
*/
|
|
56
|
+
public render(withProps: DuffelAncillariesCustomElementRenderArguments) {
|
|
57
|
+
if (!this.root) {
|
|
58
|
+
throw "It was not possible to render `duffel-ancillaries` because `this.root` is missing.";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.root.render(
|
|
62
|
+
<DuffelAncillaries
|
|
63
|
+
{...withProps}
|
|
64
|
+
onPayloadReady={(data, metadata) => {
|
|
65
|
+
this.dispatchEvent(
|
|
66
|
+
new CustomEvent("onPayloadReady", {
|
|
67
|
+
detail: { data, metadata },
|
|
68
|
+
composed: true,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
window.customElements.get(CUSTOM_ELEMENT_TAG) ||
|
|
78
|
+
window.customElements.define(
|
|
79
|
+
CUSTOM_ELEMENT_TAG,
|
|
80
|
+
DuffelAncillariesCustomElement
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
function tryToGetDuffelAncillariesCustomElement(
|
|
84
|
+
caller: string
|
|
85
|
+
): DuffelAncillariesCustomElement {
|
|
86
|
+
const element =
|
|
87
|
+
document.querySelector<DuffelAncillariesCustomElement>(CUSTOM_ELEMENT_TAG);
|
|
88
|
+
if (!element) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Could not find duffel-ancillaries element in the DOM. Maybe you need to call ${caller} after 'window.onload'?`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return element;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function renderDuffelAncillariesCustomElement(
|
|
97
|
+
props: DuffelAncillariesCustomElementRenderArguments
|
|
98
|
+
) {
|
|
99
|
+
const element = tryToGetDuffelAncillariesCustomElement(
|
|
100
|
+
"renderDuffelAncillariesCustomElement"
|
|
101
|
+
);
|
|
102
|
+
element.render(props);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type OnPayloadReadyCustomEvent = CustomEvent<{
|
|
106
|
+
data: CreateOrderPayload;
|
|
107
|
+
metadata: OnPayloadReadyMetadata;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
export function onDuffelAncillariesPayloadReady(
|
|
111
|
+
onPayloadReady: OnPayloadReady
|
|
112
|
+
) {
|
|
113
|
+
const element = tryToGetDuffelAncillariesCustomElement(
|
|
114
|
+
"onDuffelAncillariesPayloadReady"
|
|
115
|
+
);
|
|
116
|
+
const eventListener = (event: OnPayloadReadyCustomEvent) => {
|
|
117
|
+
onPayloadReady(event.detail.data, event.detail.metadata);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// using `as EventListener` here because typescript doesn't know the event type for `onPayloadReady`
|
|
121
|
+
// There's a few different suggestions to resolve this seemed good enough
|
|
122
|
+
// You can learn more here: https://github.com/microsoft/TypeScript/issues/28357
|
|
123
|
+
element.addEventListener("onPayloadReady", eventListener as EventListener);
|
|
124
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { captureErrorInSentry } from "@lib/captureErrorInSentry";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { NonIdealState } from "./NonIdealState";
|
|
5
|
+
|
|
6
|
+
export class ErrorBoundary extends React.Component<{
|
|
7
|
+
children: React.ReactNode | React.ReactNode[];
|
|
8
|
+
}> {
|
|
9
|
+
state = { hasError: false };
|
|
10
|
+
|
|
11
|
+
static getDerivedStateFromError() {
|
|
12
|
+
// Update state so the next render will show the fallback UI.
|
|
13
|
+
return { hasError: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
componentDidCatch(error: Error, context: Record<any, any>) {
|
|
17
|
+
// You can also log the error to an error reporting service
|
|
18
|
+
captureErrorInSentry(error, context);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render() {
|
|
22
|
+
if (this.state.hasError) {
|
|
23
|
+
return (
|
|
24
|
+
<NonIdealState>
|
|
25
|
+
<p style={{ marginBlock: "0" }} className="p1--semibold">
|
|
26
|
+
We ran into an error
|
|
27
|
+
</p>
|
|
28
|
+
<p
|
|
29
|
+
className="p1--regular"
|
|
30
|
+
style={{
|
|
31
|
+
color: "var(--GREY-600)",
|
|
32
|
+
marginBlock: "12px",
|
|
33
|
+
textAlign: "center",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
Please try reloading. If the problem persists reach out to our
|
|
37
|
+
support team.
|
|
38
|
+
</p>
|
|
39
|
+
<div>
|
|
40
|
+
<Button
|
|
41
|
+
variant="outlined"
|
|
42
|
+
iconBefore="autorenew"
|
|
43
|
+
onClick={() => location.reload()}
|
|
44
|
+
>
|
|
45
|
+
Try again
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</NonIdealState>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.props.children;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Button } from "./Button";
|
|
3
|
+
import { NonIdealState } from "./NonIdealState";
|
|
4
|
+
|
|
5
|
+
export const FetchOfferErrorState: React.FC<{
|
|
6
|
+
height: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}> = ({ height, message }) => (
|
|
9
|
+
<NonIdealState style={{ minHeight: height }}>
|
|
10
|
+
<p style={{ marginBlock: "0" }} className="p1--semibold">
|
|
11
|
+
Failed to load extras
|
|
12
|
+
</p>
|
|
13
|
+
<p
|
|
14
|
+
className="p2--regular"
|
|
15
|
+
style={{
|
|
16
|
+
color: "var(--GREY-600)",
|
|
17
|
+
marginBlock: "12px",
|
|
18
|
+
textAlign: "center",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
{message
|
|
22
|
+
? message
|
|
23
|
+
: "Please try reloading. If the problem persists reach out to our support team."}
|
|
24
|
+
</p>
|
|
25
|
+
<div>
|
|
26
|
+
<Button
|
|
27
|
+
variant="outlined"
|
|
28
|
+
onClick={() => location.reload()}
|
|
29
|
+
iconBefore="autorenew"
|
|
30
|
+
>
|
|
31
|
+
Try again
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
</NonIdealState>
|
|
35
|
+
);
|