@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.
Files changed (325) hide show
  1. package/.circleci/config.yml +67 -0
  2. package/.eslintrc.js +34 -0
  3. package/.github/renovate.json +16 -0
  4. package/.github/workflows/autoapprove.yml +18 -0
  5. package/.github/workflows/release.yml +86 -0
  6. package/.husky/post-commit +4 -0
  7. package/.husky/pre-commit +4 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierignore +2 -0
  10. package/.storybook/Storyshots.test.js +3 -0
  11. package/.storybook/__snapshots__/Storyshots.test.js.snap +48133 -0
  12. package/.storybook/main.ts +33 -0
  13. package/.storybook/preview.tsx +28 -0
  14. package/.tool-versions +1 -0
  15. package/README.md +129 -16
  16. package/__mocks__/styleMock.js +6 -0
  17. package/babel.config.js +20 -0
  18. package/commitlint.config.js +4 -0
  19. package/config/esbuild.base.config.js +14 -0
  20. package/config/esbuild.cdn.config.js +50 -0
  21. package/config/esbuild.dev.config.js +45 -0
  22. package/config/esbuild.react.config.js +42 -0
  23. package/jest.config.ts +14 -0
  24. package/package.json +123 -192
  25. package/react-dist/components/AnimatedLoaderEllipsis.d.ts +2 -0
  26. package/react-dist/components/Button.d.ts +23 -0
  27. package/react-dist/components/Card.d.ts +14 -0
  28. package/react-dist/components/Counter.d.ts +10 -0
  29. package/react-dist/components/DuffelAncillaries.d.ts +3 -0
  30. package/react-dist/components/DuffelAncillariesCustomElement.d.ts +13 -0
  31. package/react-dist/components/ErrorBoundary.d.ts +13 -0
  32. package/react-dist/components/FetchOfferErrorState.d.ts +5 -0
  33. package/react-dist/components/Icon.d.ts +44 -0
  34. package/react-dist/components/IconButton.d.ts +16 -0
  35. package/react-dist/components/Modal.d.ts +11 -0
  36. package/react-dist/components/NonIdealState.d.ts +4 -0
  37. package/react-dist/components/Stamp.d.ts +7 -0
  38. package/react-dist/components/Tabs.d.ts +16 -0
  39. package/react-dist/components/bags/BaggageSelectionCard.d.ts +11 -0
  40. package/react-dist/components/bags/BaggageSelectionController.d.ts +13 -0
  41. package/react-dist/components/bags/BaggageSelectionModal.d.ts +11 -0
  42. package/react-dist/components/bags/BaggageSelectionModalBody.d.ts +11 -0
  43. package/react-dist/components/bags/BaggageSelectionModalBodyPassenger.d.ts +13 -0
  44. package/react-dist/components/bags/BaggageSelectionModalFooter.d.ts +14 -0
  45. package/react-dist/components/bags/BaggageSelectionModalHeader.d.ts +9 -0
  46. package/react-dist/components/bags/IncludedBaggageBanner.d.ts +7 -0
  47. package/react-dist/components/cancel_for_any_reason/CfarSelectionCard.d.ts +10 -0
  48. package/react-dist/components/cancel_for_any_reason/CfarSelectionModal.d.ts +11 -0
  49. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBody.d.ts +7 -0
  50. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.d.ts +4 -0
  51. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalFooter.d.ts +11 -0
  52. package/react-dist/components/cancel_for_any_reason/CfarSelectionModalHeader.d.ts +2 -0
  53. package/react-dist/components/seats/Amenity.d.ts +6 -0
  54. package/react-dist/components/seats/DeckSelect.d.ts +15 -0
  55. package/react-dist/components/seats/Element.d.ts +15 -0
  56. package/react-dist/components/seats/EmptyElement.d.ts +2 -0
  57. package/react-dist/components/seats/ExitElement.d.ts +6 -0
  58. package/react-dist/components/seats/Legend.d.ts +12 -0
  59. package/react-dist/components/seats/Row.d.ts +13 -0
  60. package/react-dist/components/seats/RowSection.d.ts +17 -0
  61. package/react-dist/components/seats/SeatElement.d.ts +13 -0
  62. package/react-dist/components/seats/SeatInfo.d.ts +7 -0
  63. package/react-dist/components/seats/SeatMap.d.ts +12 -0
  64. package/react-dist/components/seats/SeatMapUnavailable.d.ts +2 -0
  65. package/react-dist/components/seats/SeatSelectionCard.d.ts +13 -0
  66. package/react-dist/components/seats/SeatSelectionModal.d.ts +13 -0
  67. package/react-dist/components/seats/SeatSelectionModalBody.d.ts +4 -0
  68. package/react-dist/components/seats/SeatSelectionModalFooter.d.ts +16 -0
  69. package/react-dist/components/seats/SeatSelectionModalHeader.d.ts +10 -0
  70. package/react-dist/components/seats/SeatUnavailable.d.ts +5 -0
  71. package/react-dist/index.d.ts +8 -0
  72. package/react-dist/index.js +36 -0
  73. package/react-dist/index.js.map +7 -0
  74. package/react-dist/lib/captureErrorInSentry.d.ts +1 -0
  75. package/react-dist/lib/compileCreateOrderPayload.d.ts +14 -0
  76. package/react-dist/lib/createPriceFormatters.d.ts +12 -0
  77. package/react-dist/lib/fetchFromDuffelAPI.d.ts +1 -0
  78. package/react-dist/lib/fetchFromFixtures.d.ts +4 -0
  79. package/react-dist/lib/formatAvailableServices.d.ts +12 -0
  80. package/react-dist/lib/formatDate.d.ts +2 -0
  81. package/react-dist/lib/formatSeatMaps.d.ts +4 -0
  82. package/react-dist/lib/getBaggageServiceDescription.d.ts +2 -0
  83. package/react-dist/lib/getCabinsForSegmentAndDeck.d.ts +2 -0
  84. package/react-dist/lib/getCurrencyForSeatMaps.d.ts +10 -0
  85. package/react-dist/lib/getCurrencyForServices.d.ts +11 -0
  86. package/react-dist/lib/getFirstSeatElementMatchingCriteria.d.ts +3 -0
  87. package/react-dist/lib/getPassengerBySegmentList.d.ts +6 -0
  88. package/react-dist/lib/getPassengerInitials.d.ts +1 -0
  89. package/react-dist/lib/getPassengerMapById.d.ts +3 -0
  90. package/react-dist/lib/getPassengerName.d.ts +3 -0
  91. package/react-dist/lib/getRowNumber.d.ts +2 -0
  92. package/react-dist/lib/getSegmentList.d.ts +2 -0
  93. package/react-dist/lib/getServicePriceMapById.d.ts +3 -0
  94. package/react-dist/lib/getSymbols.d.ts +2 -0
  95. package/react-dist/lib/getTotalAmountForServices.d.ts +6 -0
  96. package/react-dist/lib/getTotalQuantity.d.ts +2 -0
  97. package/react-dist/lib/hasService.d.ts +2 -0
  98. package/react-dist/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.d.ts +3 -0
  99. package/react-dist/lib/hasWings.d.ts +2 -0
  100. package/react-dist/lib/isBaggageService.d.ts +2 -0
  101. package/react-dist/lib/isCancelForAnyReasonService.d.ts +2 -0
  102. package/react-dist/lib/isFixtureOfferId.d.ts +2 -0
  103. package/react-dist/lib/isPayloadComplete.d.ts +2 -0
  104. package/react-dist/lib/isSeatElement.d.ts +2 -0
  105. package/react-dist/lib/logging.d.ts +53 -0
  106. package/react-dist/lib/moneyStringFormatter.d.ts +8 -0
  107. package/react-dist/lib/offerIsExpired.d.ts +2 -0
  108. package/react-dist/lib/retrieveOffer.d.ts +2 -0
  109. package/react-dist/lib/retrieveOfferFromDuffelAPI.d.ts +1 -0
  110. package/react-dist/lib/retrieveSeatMaps.d.ts +2 -0
  111. package/react-dist/lib/retrieveSeatMapsFromDuffelAPI.d.ts +1 -0
  112. package/react-dist/lib/setBodyScrollability.d.ts +1 -0
  113. package/react-dist/lib/validateProps.d.ts +7 -0
  114. package/react-dist/lib/withPlural.d.ts +1 -0
  115. package/react-dist/types/Aircraft.d.ts +14 -0
  116. package/react-dist/types/Airline.d.ts +14 -0
  117. package/react-dist/types/Airport.d.ts +44 -0
  118. package/react-dist/types/City.d.ts +18 -0
  119. package/react-dist/types/CreateOrderPayload.d.ts +72 -0
  120. package/react-dist/types/CurrencyConversion.d.ts +10 -0
  121. package/react-dist/types/DuffelAncillariesProps.d.ts +70 -0
  122. package/react-dist/types/Offer.d.ts +711 -0
  123. package/react-dist/types/Order.d.ts +8 -0
  124. package/react-dist/types/Place.d.ts +8 -0
  125. package/react-dist/types/SeatMap.d.ts +190 -0
  126. package/react-dist/types/index.d.ts +11 -0
  127. package/scripts/generate-fixture.ts +195 -0
  128. package/scripts/upload-to-cdn.sh +39 -0
  129. package/scripts.tsconfig.json +11 -0
  130. package/src/components/AnimatedLoaderEllipsis.tsx +5 -0
  131. package/src/components/Button.tsx +62 -0
  132. package/src/components/Card.tsx +126 -0
  133. package/src/components/Counter.tsx +40 -0
  134. package/src/components/DuffelAncillaries.tsx +346 -0
  135. package/src/components/DuffelAncillariesCustomElement.tsx +124 -0
  136. package/src/components/ErrorBoundary.tsx +54 -0
  137. package/src/components/FetchOfferErrorState.tsx +35 -0
  138. package/src/components/Icon.tsx +151 -0
  139. package/src/components/IconButton.tsx +42 -0
  140. package/src/components/Modal.tsx +36 -0
  141. package/src/components/NonIdealState.tsx +28 -0
  142. package/src/components/Stamp.tsx +29 -0
  143. package/src/components/Tabs.tsx +36 -0
  144. package/src/components/bags/BaggageSelectionCard.tsx +96 -0
  145. package/src/components/bags/BaggageSelectionController.tsx +88 -0
  146. package/src/components/bags/BaggageSelectionModal.tsx +81 -0
  147. package/src/components/bags/BaggageSelectionModalBody.tsx +60 -0
  148. package/src/components/bags/BaggageSelectionModalBodyPassenger.tsx +122 -0
  149. package/src/components/bags/BaggageSelectionModalFooter.tsx +81 -0
  150. package/src/components/bags/BaggageSelectionModalHeader.tsx +76 -0
  151. package/src/components/bags/IncludedBaggageBanner.tsx +51 -0
  152. package/src/components/cancel_for_any_reason/CfarSelectionCard.tsx +90 -0
  153. package/src/components/cancel_for_any_reason/CfarSelectionModal.tsx +63 -0
  154. package/src/components/cancel_for_any_reason/CfarSelectionModalBody.tsx +56 -0
  155. package/src/components/cancel_for_any_reason/CfarSelectionModalBodyListItem.tsx +11 -0
  156. package/src/components/cancel_for_any_reason/CfarSelectionModalFooter.tsx +74 -0
  157. package/src/components/cancel_for_any_reason/CfarSelectionModalHeader.tsx +9 -0
  158. package/src/components/seats/Amenity.tsx +21 -0
  159. package/src/components/seats/DeckSelect.tsx +27 -0
  160. package/src/components/seats/Element.tsx +52 -0
  161. package/src/components/seats/EmptyElement.tsx +5 -0
  162. package/src/components/seats/ExitElement.tsx +17 -0
  163. package/src/components/seats/Legend.tsx +60 -0
  164. package/src/components/seats/Row.tsx +47 -0
  165. package/src/components/seats/RowSection.tsx +75 -0
  166. package/src/components/seats/SeatElement.tsx +120 -0
  167. package/src/components/seats/SeatInfo.tsx +32 -0
  168. package/src/components/seats/SeatMap.tsx +81 -0
  169. package/src/components/seats/SeatMapUnavailable.tsx +21 -0
  170. package/src/components/seats/SeatSelectionCard.tsx +103 -0
  171. package/src/components/seats/SeatSelectionModal.tsx +142 -0
  172. package/src/components/seats/SeatSelectionModalBody.tsx +13 -0
  173. package/src/components/seats/SeatSelectionModalFooter.tsx +82 -0
  174. package/src/components/seats/SeatSelectionModalHeader.tsx +87 -0
  175. package/src/components/seats/SeatUnavailable.tsx +14 -0
  176. package/src/examples/client-side/index.html +57 -0
  177. package/src/examples/full-stack/index.html +48 -0
  178. package/src/examples/full-stack/server.mjs +157 -0
  179. package/src/examples/just-typescript/README.md +37 -0
  180. package/src/examples/just-typescript/package.json +16 -0
  181. package/src/examples/just-typescript/src/index.html +23 -0
  182. package/src/examples/just-typescript/src/index.ts +35 -0
  183. package/src/examples/just-typescript/yarn.lock +154 -0
  184. package/src/examples/react-app/README.md +37 -0
  185. package/src/examples/react-app/package.json +20 -0
  186. package/src/examples/react-app/src/index.html +19 -0
  187. package/src/examples/react-app/src/index.tsx +43 -0
  188. package/src/examples/react-app/yarn.lock +219 -0
  189. package/src/fixtures/offers/off_0000AUde3KwTztSRK1cznH.json +497 -0
  190. package/src/fixtures/offers/off_0000AVx4lUFFKW8PsPeQeQ.json +307 -0
  191. package/src/fixtures/offers/off_1.json +497 -0
  192. package/src/fixtures/passengers/mock_passengers.ts +26 -0
  193. package/src/fixtures/seat-maps/off_0000AUde3KwTztSRK1cznH.json +6852 -0
  194. package/src/fixtures/seat-maps/off_0000AVx4lUFFKW8PsPeQeQ.json +1 -0
  195. package/src/fixtures/seat-maps/off_1.json +6852 -0
  196. package/src/index.ts +8 -0
  197. package/src/lib/captureErrorInSentry.ts +60 -0
  198. package/src/lib/compileCreateOrderPayload.ts +63 -0
  199. package/src/lib/createPriceFormatters.ts +73 -0
  200. package/src/lib/fetchFromDuffelAPI.ts +24 -0
  201. package/src/lib/fetchFromFixtures.ts +18 -0
  202. package/src/lib/formatAvailableServices.ts +91 -0
  203. package/src/lib/formatDate.ts +21 -0
  204. package/src/lib/formatSeatMaps.ts +81 -0
  205. package/src/lib/getBaggageServiceDescription.ts +44 -0
  206. package/src/lib/getCabinsForSegmentAndDeck.ts +4 -0
  207. package/src/lib/getCurrencyForSeatMaps.ts +22 -0
  208. package/src/lib/getCurrencyForServices.ts +24 -0
  209. package/src/lib/getFirstSeatElementMatchingCriteria.ts +22 -0
  210. package/src/lib/getPassengerBySegmentList.ts +10 -0
  211. package/src/lib/getPassengerInitials.ts +6 -0
  212. package/src/lib/getPassengerMapById.ts +17 -0
  213. package/src/lib/getPassengerName.ts +37 -0
  214. package/src/lib/getRowNumber.ts +16 -0
  215. package/src/lib/getSegmentList.ts +7 -0
  216. package/src/lib/getServicePriceMapById.ts +20 -0
  217. package/src/lib/getSymbols.ts +22 -0
  218. package/src/lib/getTotalAmountForServices.ts +72 -0
  219. package/src/lib/getTotalQuantity.ts +5 -0
  220. package/src/lib/hasService.ts +24 -0
  221. package/src/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.ts +35 -0
  222. package/src/lib/hasWings.ts +8 -0
  223. package/src/lib/isBaggageService.ts +8 -0
  224. package/src/lib/isCancelForAnyReasonService.ts +9 -0
  225. package/src/lib/isFixtureOfferId.ts +4 -0
  226. package/src/lib/isPayloadComplete.ts +11 -0
  227. package/src/lib/isSeatElement.ts +10 -0
  228. package/src/lib/logging.ts +100 -0
  229. package/src/lib/moneyStringFormatter.ts +34 -0
  230. package/src/lib/offerIsExpired.ts +5 -0
  231. package/src/lib/retrieveOffer.ts +49 -0
  232. package/src/lib/retrieveOfferFromDuffelAPI.ts +13 -0
  233. package/src/lib/retrieveSeatMaps.ts +50 -0
  234. package/src/lib/retrieveSeatMapsFromDuffelAPI.ts +13 -0
  235. package/src/lib/setBodyScrollability.ts +7 -0
  236. package/src/lib/validateProps.ts +37 -0
  237. package/src/lib/withPlural.ts +8 -0
  238. package/src/stories/BaggageSelectionModalHeader.stories.tsx +21 -0
  239. package/src/stories/Button.stories.tsx +60 -0
  240. package/src/stories/DuffelAncillaries.stories.tsx +126 -0
  241. package/src/stories/Icon.stories.tsx +34 -0
  242. package/src/stories/IconButton.stories.tsx +25 -0
  243. package/src/styles/colors.css +22 -0
  244. package/src/styles/components/Amenity.css +23 -0
  245. package/src/styles/components/BaggageDisplay.css +25 -0
  246. package/src/styles/components/Button.css +161 -0
  247. package/src/styles/components/Card.css +52 -0
  248. package/src/styles/components/CfarSelectionModal.css +34 -0
  249. package/src/styles/components/Counter.css +18 -0
  250. package/src/styles/components/IconButton.css +63 -0
  251. package/src/styles/components/Legend.css +58 -0
  252. package/src/styles/components/Loader.css +37 -0
  253. package/src/styles/components/LoadingState.css +81 -0
  254. package/src/styles/components/Modal.css +83 -0
  255. package/src/styles/components/PassengerSelect.css +93 -0
  256. package/src/styles/components/PassengersLayout.css +90 -0
  257. package/src/styles/components/Row.css +70 -0
  258. package/src/styles/components/Seat.css +57 -0
  259. package/src/styles/components/SeatInfo.css +61 -0
  260. package/src/styles/components/SeatMap.css +24 -0
  261. package/src/styles/components/SeatSelect.css +92 -0
  262. package/src/styles/components/Segment.css +17 -0
  263. package/src/styles/components/SelectionSegment.css +10 -0
  264. package/src/styles/components/Summary.css +70 -0
  265. package/src/styles/components/Tabs.css +49 -0
  266. package/src/styles/flex.css +5 -0
  267. package/src/styles/font-families.css +47 -0
  268. package/src/styles/global.css +50 -0
  269. package/src/styles/margin.css +3 -0
  270. package/src/styles/spacing.css +18 -0
  271. package/src/styles/transitions.css +3 -0
  272. package/src/styles/typography.css +13 -0
  273. package/src/tests/components/DuffelAncillaries.test.tsx +342 -0
  274. package/src/tests/lib/createPriceFormatters.test.tsx +152 -0
  275. package/src/tests/lib/formatAvailableServices.test.tsx +79 -0
  276. package/src/tests/lib/formatSeatMaps.test.tsx +49 -0
  277. package/src/tests/lib/getCurrencyForServices.test.tsx +44 -0
  278. package/src/tests/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.test.ts +86 -0
  279. package/src/tests/lib/logging.test.tsx +32 -0
  280. package/src/tests/lib/moneyStringFormatter.test.tsx +12 -0
  281. package/src/tests/lib/validateProps.test.tsx +57 -0
  282. package/src/types/Aircraft.ts +16 -0
  283. package/src/types/Airline.ts +16 -0
  284. package/src/types/Airport.ts +54 -0
  285. package/src/types/City.ts +21 -0
  286. package/src/types/CreateOrderPayload.ts +99 -0
  287. package/src/types/CurrencyConversion.ts +10 -0
  288. package/src/types/DuffelAncillariesProps.ts +108 -0
  289. package/src/types/Offer.ts +851 -0
  290. package/src/types/Order.ts +6 -0
  291. package/src/types/Place.ts +6 -0
  292. package/src/types/SeatMap.ts +231 -0
  293. package/src/types/index.ts +11 -0
  294. package/tsconfig.json +52 -0
  295. package/LICENSE +0 -21
  296. package/dist/AdditionalBaggage.esm.js +0 -1
  297. package/dist/AdditionalBaggage.js +0 -1
  298. package/dist/AdditionalBaggage.min.css +0 -408
  299. package/dist/AdditionalBaggage.umd.min.js +0 -2
  300. package/dist/AdditionalBaggage.umd.min.js.LICENSE.txt +0 -60
  301. package/dist/AdditionalBaggageSelection.esm.js +0 -1
  302. package/dist/AdditionalBaggageSelection.js +0 -1
  303. package/dist/AdditionalBaggageSelection.min.css +0 -744
  304. package/dist/AdditionalBaggageSelection.umd.min.js +0 -2
  305. package/dist/AdditionalBaggageSelection.umd.min.js.LICENSE.txt +0 -93
  306. package/dist/CardPayment.esm.js +0 -2
  307. package/dist/CardPayment.esm.js.LICENSE.txt +0 -6
  308. package/dist/CardPayment.js +0 -2
  309. package/dist/CardPayment.js.LICENSE.txt +0 -6
  310. package/dist/CardPayment.min.css +0 -233
  311. package/dist/CardPayment.umd.min.js +0 -2
  312. package/dist/CardPayment.umd.min.js.LICENSE.txt +0 -61
  313. package/dist/SeatSelection.esm.js +0 -1
  314. package/dist/SeatSelection.js +0 -1
  315. package/dist/SeatSelection.min.css +0 -1127
  316. package/dist/SeatSelection.umd.min.js +0 -2
  317. package/dist/SeatSelection.umd.min.js.LICENSE.txt +0 -60
  318. package/dist/duffel-components.d.ts +0 -1614
  319. package/dist/duffel-components.esm.js +0 -2
  320. package/dist/duffel-components.esm.js.LICENSE.txt +0 -6
  321. package/dist/duffel-components.js +0 -2
  322. package/dist/duffel-components.js.LICENSE.txt +0 -6
  323. package/dist/duffel-components.min.css +0 -1280
  324. package/dist/duffel-components.umd.min.js +0 -2
  325. 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
+ );