@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,20 @@
1
+ import { OfferAvailableService } from "../types/Offer";
2
+
3
+ export type ServicePriceMapById = Record<
4
+ OfferAvailableService["id"],
5
+ Pick<OfferAvailableService, "total_amount" | "total_currency">
6
+ >;
7
+
8
+ export const getServicePriceMapById = (
9
+ availableServices: OfferAvailableService[]
10
+ ): ServicePriceMapById =>
11
+ availableServices.reduce(
12
+ (all, { id, total_amount, total_currency }) => ({
13
+ [id]: {
14
+ total_amount,
15
+ total_currency,
16
+ },
17
+ ...all,
18
+ }),
19
+ {} as ServicePriceMapById
20
+ );
@@ -0,0 +1,22 @@
1
+ import {
2
+ SeatMapCabin,
3
+ SeatMapCabinRowSectionElementAmenity,
4
+ } from "../types/SeatMap";
5
+
6
+ export const getSymbols = (
7
+ cabins: SeatMapCabin[]
8
+ ): Set<SeatMapCabinRowSectionElementAmenity> => {
9
+ const results: Set<SeatMapCabinRowSectionElementAmenity> = new Set();
10
+ for (const cabin of cabins) {
11
+ for (const row of cabin.rows) {
12
+ for (const section of row.sections) {
13
+ for (const element of section.elements) {
14
+ if (element.type !== "seat" && element.type !== "empty") {
15
+ results.add(element.type);
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ return results;
22
+ };
@@ -0,0 +1,72 @@
1
+ import { CreateOrderPayloadServices } from "../types/CreateOrderPayload";
2
+ import { Offer } from "../types/Offer";
3
+ import { SeatMap } from "../types/SeatMap";
4
+ import { captureErrorInSentry } from "./captureErrorInSentry";
5
+ import {
6
+ ServicePriceMapById,
7
+ getServicePriceMapById,
8
+ } from "./getServicePriceMapById";
9
+
10
+ export const getTotalAmountForServices = (
11
+ offer: Offer,
12
+ selectedServices: CreateOrderPayloadServices,
13
+ seatMaps?: SeatMap[]
14
+ ): number => {
15
+ if (!offer || selectedServices.length === 0) return 0;
16
+ const servicePriceMap = getServicePriceMapById(offer.available_services);
17
+ return getTotalAmountForServicesWithPriceMap(
18
+ servicePriceMap,
19
+ selectedServices,
20
+ seatMaps
21
+ );
22
+ };
23
+
24
+ export const getTotalAmountForServicesWithPriceMap = (
25
+ servicePriceMap: ServicePriceMapById,
26
+ selectedServices: CreateOrderPayloadServices,
27
+ seatMaps?: SeatMap[]
28
+ ) =>
29
+ selectedServices.reduce(
30
+ (total, { quantity, id }) => {
31
+ let newTotal = total;
32
+
33
+ if (id in servicePriceMap) {
34
+ newTotal += quantity * +servicePriceMap[id].total_amount;
35
+ } else if (seatMaps) {
36
+ newTotal += quantity * getTotalAmountFromSeatMaps(id, seatMaps);
37
+ } else {
38
+ captureErrorInSentry(
39
+ new Error(
40
+ `The service id provided could not be found in neither the offer nor the seat maps. Service id: ${id}`
41
+ )
42
+ );
43
+ }
44
+
45
+ return newTotal;
46
+ },
47
+
48
+ 0
49
+ );
50
+
51
+ const getTotalAmountFromSeatMaps = (serviceId: string, seatMaps: SeatMap[]) => {
52
+ for (const seatMap of seatMaps) {
53
+ for (const cabin of seatMap.cabins) {
54
+ for (const row of cabin.rows) {
55
+ for (const section of row.sections) {
56
+ for (const element of section.elements) {
57
+ if (
58
+ element.type === "seat" &&
59
+ Array.isArray(element.available_services)
60
+ ) {
61
+ const serviceMatch = element.available_services.find(
62
+ (service) => service.id === serviceId
63
+ );
64
+ if (serviceMatch) return +serviceMatch.total_amount;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return 0;
72
+ };
@@ -0,0 +1,5 @@
1
+ import { CreateOrderPayloadServices } from "../types/CreateOrderPayload";
2
+
3
+ export const getTotalQuantity = (
4
+ fromSelectedServices: CreateOrderPayloadServices
5
+ ) => fromSelectedServices.reduce((total, { quantity }) => total + quantity, 0);
@@ -0,0 +1,24 @@
1
+ import { Offer, OfferAvailableServiceType } from "../types/Offer";
2
+ import { isBaggageService } from "./isBaggageService";
3
+ import { isCancelForAnyReasonService } from "./isCancelForAnyReasonService";
4
+
5
+ const checkFunctionsMap = {
6
+ baggage: isBaggageService,
7
+ cancel_for_any_reason: isCancelForAnyReasonService,
8
+ };
9
+
10
+ export const hasService = (
11
+ offer: Offer | undefined,
12
+ type: OfferAvailableServiceType
13
+ ) => {
14
+ const checkFunction = checkFunctionsMap[type];
15
+ if (!checkFunction) throw new Error(`Unknown service type: ${type}`);
16
+
17
+ return (
18
+ offer &&
19
+ Array.isArray(offer.available_services) &&
20
+ offer.available_services.some(
21
+ (service) => checkFunction(service) && service.maximum_quantity > 0
22
+ )
23
+ );
24
+ };
@@ -0,0 +1,35 @@
1
+ import { CreateOrderPayloadServices } from "../types/CreateOrderPayload";
2
+ import { OfferAvailableServiceBaggage } from "../types/Offer";
3
+
4
+ export const hasServiceOfSameMetadataTypeAlreadyBeenSelected = (
5
+ selectedServices: CreateOrderPayloadServices,
6
+ segmentId: string,
7
+ passengerId: string,
8
+ availableService: OfferAvailableServiceBaggage
9
+ ) =>
10
+ selectedServices.some((selectedService) => {
11
+ if (selectedService.id === availableService.id) {
12
+ // if the selected service is the one on the counter, don't disable it
13
+ // the max quantity will be availableService.maximum_quantity
14
+ return false;
15
+ } else if (
16
+ selectedService.serviceInformation?.type !== "carry_on" &&
17
+ selectedService.serviceInformation?.type !== "checked"
18
+ ) {
19
+ return false;
20
+ } else if (selectedService.serviceInformation?.segmentId !== segmentId) {
21
+ // if the selected service doesn't belong to the same segment, don't disable it
22
+ return false;
23
+ } else if (
24
+ // if the selected service doesn't belong to the same passenger, don't disable it
25
+ selectedService.serviceInformation?.passengerId !== passengerId
26
+ ) {
27
+ return false;
28
+ } else {
29
+ // if this service selection controller is for the same passenger and segment, disable it
30
+ return (
31
+ selectedService.serviceInformation?.type ===
32
+ availableService.metadata.type
33
+ );
34
+ }
35
+ });
@@ -0,0 +1,8 @@
1
+ import { SeatMapCabin } from "../types/SeatMap";
2
+
3
+ export const hasWings = (cabin: SeatMapCabin, rowIndex: number) =>
4
+ Boolean(
5
+ cabin.wings &&
6
+ cabin.wings.first_row_index <= rowIndex &&
7
+ cabin.wings.last_row_index >= rowIndex
8
+ );
@@ -0,0 +1,8 @@
1
+ import {
2
+ OfferAvailableBaggageService,
3
+ OfferAvailableService,
4
+ } from "../types/Offer";
5
+
6
+ export const isBaggageService = (
7
+ service: OfferAvailableService
8
+ ): service is OfferAvailableBaggageService => service.type === "baggage";
@@ -0,0 +1,9 @@
1
+ import {
2
+ OfferAvailableService,
3
+ OfferAvailableServiceCancelForAnyReason,
4
+ } from "../types/Offer";
5
+
6
+ export const isCancelForAnyReasonService = (
7
+ service: OfferAvailableService
8
+ ): service is OfferAvailableServiceCancelForAnyReason =>
9
+ service.type === "cancel_for_any_reason";
@@ -0,0 +1,4 @@
1
+ import { Offer } from "../types/Offer";
2
+
3
+ export const isFixtureOfferId = (offerId: Offer["id"]): boolean =>
4
+ offerId.startsWith("fixture_off_");
@@ -0,0 +1,11 @@
1
+ import { CreateOrderPayload } from "../types/CreateOrderPayload";
2
+
3
+ export const isPayloadComplete = (
4
+ payload: Partial<CreateOrderPayload>
5
+ ): payload is CreateOrderPayload =>
6
+ "selected_offers" in payload &&
7
+ "passengers" in payload &&
8
+ "services" in payload &&
9
+ "payments" in payload &&
10
+ "type" in payload &&
11
+ "metadata" in payload;
@@ -0,0 +1,10 @@
1
+ import {
2
+ SeatMapCabinRowSectionElement,
3
+ SeatMapCabinRowSectionElementSeat,
4
+ } from "../types/SeatMap";
5
+
6
+ export function isSeatElement(
7
+ element: SeatMapCabinRowSectionElement
8
+ ): element is SeatMapCabinRowSectionElementSeat {
9
+ return element.type === "seat";
10
+ }
@@ -0,0 +1,100 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ /**
4
+ * The functions in this file are used to enable logging.
5
+ *
6
+ * Usage:
7
+ *
8
+ * In your app's outermost container, import the LogContext and wrap your app in it:
9
+ *
10
+ * ```jsx
11
+ * import { LogContext, initializeLogger } from '@lib/logging'
12
+ *
13
+ * const logger = initializeLogger(props.debugMode || false)
14
+ *
15
+ * <LogContext.Provider value={logger}>
16
+ * ...
17
+ * </LogContext.Provider>
18
+ * ```
19
+ *
20
+ * Then in your components nested within the above container, import the useLog hook and use it:
21
+ *
22
+ * import { useLog } from '@lib/logging'
23
+ *
24
+ * const { log, logGroup } = useLog()
25
+ * log('This is a log message')
26
+ * logGroup('These messages will be grouped together', ['This is a log message', 'This is another log message'])
27
+ */
28
+
29
+ const initializeLogger = (debugMode: boolean) => {
30
+ if (debugMode) {
31
+ log(
32
+ `\n\nDebug mode is enabled. Information about your setup will be printed to the console.\n\nIf you do not want to enable debug mode (for example in a production environment), pass "debug: false" when initializing this component.\n\nLearn more about the Ancillaries component:\nhttp://duffel.com/docs/guides/ancillaries-component`
33
+ );
34
+ }
35
+
36
+ // We return functions that do nothing because it allows consumers
37
+ // of this function to not have to check if the logger is enabled or not.
38
+ // If we returned undefined, consumers would have to do something like:
39
+ // if (log) { log('message') }
40
+ //
41
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
42
+ const noop = () => {};
43
+
44
+ return {
45
+ log: debugMode ? log : noop,
46
+ logGroup: debugMode ? logGroup : noop,
47
+ };
48
+ };
49
+
50
+ const MESSAGE_PREFIX = "[Duffel Ancillaries] ";
51
+
52
+ /**
53
+ * Log a message to the console. Messages will be prefixed with "[Duffel Ancillaries]".
54
+ * @param message The message to print to the console.
55
+ */
56
+ const log = (message: any) => {
57
+ console.log(MESSAGE_PREFIX, message);
58
+ };
59
+
60
+ /**
61
+ * Log a series of messages to the console inside a collapsible group.
62
+ * @param groupName The name of the group of messages. This will be prefixed with "[Duffel Ancillaries]".
63
+ * @param messages An array of messages to print to the console, inside the group.
64
+ */
65
+ function logGroup(groupName: string, messages: any[]): void;
66
+
67
+ /**
68
+ * Log a series of messages to the console inside a collapsible group.
69
+ * @param groupName The name of the group of messages. This will be prefixed with "[Duffel Ancillaries]".
70
+ * @param object An object to print to the console, inside the group.
71
+ */
72
+ function logGroup(groupName: string, object: { [key: string]: any }): void;
73
+
74
+ // Overloaded function implementation.
75
+ // https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
76
+ function logGroup(
77
+ groupName: string,
78
+ messagesOrObject: any[] | { [key: string]: any }
79
+ ): void {
80
+ console.groupCollapsed(MESSAGE_PREFIX, groupName);
81
+
82
+ let transformedMessagesOrObject = [];
83
+ if (Array.isArray(messagesOrObject)) {
84
+ transformedMessagesOrObject = messagesOrObject;
85
+ } else {
86
+ transformedMessagesOrObject = Object.entries(messagesOrObject).map(
87
+ ([key, value]) => ({ property: key, value })
88
+ );
89
+ }
90
+ transformedMessagesOrObject.forEach((message) => {
91
+ console.log(message);
92
+ });
93
+
94
+ console.groupEnd();
95
+ }
96
+
97
+ const LogContext = createContext(initializeLogger(false));
98
+ const useLog = () => useContext(LogContext);
99
+
100
+ export { LogContext, initializeLogger, useLog };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Return a function to format a number into a money amount for a given currency
3
+ *
4
+ * @param currency The ISO-4217 currency code to be used by the formatter
5
+ */
6
+ export const moneyStringFormatter = (
7
+ currency: string,
8
+ locale = "en-GB",
9
+ options: { [option: string]: string } = {}
10
+ ) => {
11
+ return (value: number) => {
12
+ try {
13
+ const formatter = new Intl.NumberFormat(locale, {
14
+ style: "currency",
15
+ currency,
16
+ ...options,
17
+ });
18
+ const numberFormatted = formatter.format(value);
19
+ const signFormatted =
20
+ options &&
21
+ options["signDisplay"] &&
22
+ (options["signDisplay"] === "always" ||
23
+ (options["signDisplay"] === "exceptZero" && value !== 0))
24
+ ? numberFormatted.replace(/^([+-])/, "$1 ")
25
+ : numberFormatted;
26
+
27
+ return signFormatted;
28
+ } catch (error) {
29
+ // If the currency is not supported by the browser, we return the value with the currency code.
30
+ // This allows us to support made-up currencies, like "1000 Duffel Points"
31
+ return `${value} ${currency}`;
32
+ }
33
+ };
34
+ };
@@ -0,0 +1,5 @@
1
+ import { Offer } from "../types/Offer";
2
+
3
+ export const offerIsExpired = (offer: Offer) => {
4
+ return offer.expires_at && new Date(offer.expires_at) < new Date();
5
+ };
@@ -0,0 +1,49 @@
1
+ import { Offer } from "../types/Offer";
2
+ import { captureErrorInSentry } from "./captureErrorInSentry";
3
+ import { importFromOfferFixtures } from "./fetchFromFixtures";
4
+ import { isFixtureOfferId } from "./isFixtureOfferId";
5
+ import { retrieveOfferFromDuffelAPI } from "./retrieveOfferFromDuffelAPI";
6
+
7
+ export async function retrieveOffer(
8
+ offer_id: string,
9
+ client_key: string | null,
10
+ onError: (error: string) => void,
11
+ setIsLoading: (isLoading: boolean) => void,
12
+ onOfferReady: (offer: Offer) => void
13
+ ) {
14
+ setIsLoading(true);
15
+ const useFixture = isFixtureOfferId(offer_id);
16
+ offer_id = useFixture ? offer_id.replace("fixture_", "") : offer_id;
17
+
18
+ if (useFixture) {
19
+ return importFromOfferFixtures(offer_id).then((offer) => {
20
+ setIsLoading(false);
21
+ onOfferReady(offer);
22
+ });
23
+ }
24
+
25
+ if (!client_key) {
26
+ throw new Error(
27
+ "Attemptted to retrieve seat maps but the client key is missing"
28
+ );
29
+ }
30
+
31
+ try {
32
+ const data = await retrieveOfferFromDuffelAPI(offer_id, client_key);
33
+ return onOfferReady(data);
34
+ } catch (error) {
35
+ let message = "An unknown error occurred while retrieving the offer.";
36
+ if (error instanceof Error) {
37
+ message = error.message;
38
+ if (error.message.includes("ECONNREFUSED")) {
39
+ message = "The Duffel API is not available. Please try again later.";
40
+ }
41
+ captureErrorInSentry(error, { offer_id });
42
+ } else {
43
+ captureErrorInSentry(new Error(message), { offer_id });
44
+ }
45
+ onError(message);
46
+ } finally {
47
+ setIsLoading(false);
48
+ }
49
+ }
@@ -0,0 +1,13 @@
1
+ import { fetchFromDuffelAPI } from "./fetchFromDuffelAPI";
2
+
3
+ export async function retrieveOfferFromDuffelAPI(
4
+ offer_id: string,
5
+ client_key: string
6
+ ) {
7
+ const getOfferResponse = await fetchFromDuffelAPI(
8
+ client_key,
9
+ `offers/${offer_id}?return_available_services=true`
10
+ );
11
+
12
+ return getOfferResponse.data;
13
+ }
@@ -0,0 +1,50 @@
1
+ import { SeatMap } from "../types/SeatMap";
2
+ import { captureErrorInSentry } from "./captureErrorInSentry";
3
+ import { importFromSeatMapsFixtures } from "./fetchFromFixtures";
4
+ import { isFixtureOfferId } from "./isFixtureOfferId";
5
+ import { retrieveSeatMapsFromDuffelAPI } from "./retrieveSeatMapsFromDuffelAPI";
6
+
7
+ export async function retrieveSeatMaps(
8
+ offer_id: string,
9
+ client_key: string | null,
10
+ onError: (error: string) => void,
11
+ setIsLoading: (isLoading: boolean) => void,
12
+ onSeatMapReady: (seatMaps: SeatMap[]) => void
13
+ ) {
14
+ setIsLoading(true);
15
+
16
+ const useFixture = isFixtureOfferId(offer_id);
17
+ offer_id = useFixture ? offer_id.replace("fixture_", "") : offer_id;
18
+
19
+ if (useFixture) {
20
+ return importFromSeatMapsFixtures(offer_id).then((seatMaps) => {
21
+ setIsLoading(false);
22
+ onSeatMapReady(seatMaps);
23
+ });
24
+ }
25
+
26
+ if (!client_key) {
27
+ throw new Error(
28
+ "Attemptted to retrieve seat maps but the client key is missing"
29
+ );
30
+ }
31
+
32
+ try {
33
+ const data = await retrieveSeatMapsFromDuffelAPI(offer_id, client_key);
34
+ onSeatMapReady(data);
35
+ } catch (error) {
36
+ let message = "An unknown error occurred while retrieving the offer.";
37
+ if (error instanceof Error) {
38
+ message = error.message;
39
+ if (error.message.includes("ECONNREFUSED")) {
40
+ message = "The Duffel API is not available. Please try again later.";
41
+ }
42
+ captureErrorInSentry(error, { offer_id });
43
+ } else {
44
+ captureErrorInSentry(new Error(message), { offer_id });
45
+ }
46
+ onError(message);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ }
@@ -0,0 +1,13 @@
1
+ import { fetchFromDuffelAPI } from "./fetchFromDuffelAPI";
2
+
3
+ export async function retrieveSeatMapsFromDuffelAPI(
4
+ offer_id: string,
5
+ client_key: string
6
+ ) {
7
+ const getSeatMapResponse = await fetchFromDuffelAPI(
8
+ client_key,
9
+ `offers/${offer_id}/seat_maps`
10
+ );
11
+
12
+ return getSeatMapResponse.data;
13
+ }
@@ -0,0 +1,7 @@
1
+ export const setBodyScrollability = (shouldScroll: boolean) => {
2
+ if (shouldScroll) {
3
+ document.body.style.overflow = "";
4
+ } else {
5
+ document.body.style.overflow = "hidden";
6
+ }
7
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ DuffelAncillariesProps,
3
+ DuffelAncillariesPropsWithClientKeyAndOfferId,
4
+ DuffelAncillariesPropsWithOfferIdForFixture,
5
+ DuffelAncillariesPropsWithOffersAndSeatMaps,
6
+ DuffelAncillariesPropWithOfferAndClientKey,
7
+ } from "../types/DuffelAncillariesProps";
8
+
9
+ export const areDuffelAncillariesPropsValid = (props: DuffelAncillariesProps) =>
10
+ hasCommonRequiredProps(props) &&
11
+ (isDuffelAncillariesPropsWithOfferIdForFixture(props) ||
12
+ isDuffelAncillariesPropsWithClientKeyAndOfferId(props) ||
13
+ isDuffelAncillariesPropsWithOfferAndSeatMaps(props) ||
14
+ isDuffelAncillariesPropsWithOfferAndClientKey(props));
15
+
16
+ export const hasCommonRequiredProps = (props: DuffelAncillariesProps) =>
17
+ "onPayloadReady" in props && "passengers" in props && "services" in props;
18
+
19
+ export const isDuffelAncillariesPropsWithOfferIdForFixture = (
20
+ props: DuffelAncillariesProps
21
+ ): props is DuffelAncillariesPropsWithOfferIdForFixture =>
22
+ "offer_id" in props && props.offer_id.startsWith("fixture_");
23
+
24
+ export const isDuffelAncillariesPropsWithClientKeyAndOfferId = (
25
+ props: DuffelAncillariesProps
26
+ ): props is DuffelAncillariesPropsWithClientKeyAndOfferId =>
27
+ "offer_id" in props && "client_key" in props;
28
+
29
+ export const isDuffelAncillariesPropsWithOfferAndSeatMaps = (
30
+ props: DuffelAncillariesProps
31
+ ): props is DuffelAncillariesPropsWithOffersAndSeatMaps =>
32
+ "offer" in props && "seat_maps" in props;
33
+
34
+ export const isDuffelAncillariesPropsWithOfferAndClientKey = (
35
+ props: DuffelAncillariesProps
36
+ ): props is DuffelAncillariesPropWithOfferAndClientKey =>
37
+ "offer" in props && "client_key" in props;
@@ -0,0 +1,8 @@
1
+ export const withPlural = (
2
+ totalQuantity: number,
3
+ singular: string,
4
+ plural: string
5
+ ) => {
6
+ if (totalQuantity == 1) return `${totalQuantity} ${singular}`;
7
+ else return `${totalQuantity} ${plural}`;
8
+ };
@@ -0,0 +1,21 @@
1
+ import { BaggageSelectionModalHeader } from "@components/bags/BaggageSelectionModalHeader";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { OfferSliceSegment } from "../types/Offer";
4
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
5
+ const offer = require("../fixtures/offers/off_0000AUde3KwTztSRK1cznH.json");
6
+
7
+ export default {
8
+ title: "BaggageSelectionModalHeader",
9
+ component: BaggageSelectionModalHeader,
10
+ } as Meta;
11
+
12
+ type Story = StoryObj<typeof BaggageSelectionModalHeader>;
13
+
14
+ export const FirstStory: Story = {
15
+ args: {
16
+ segmentCount: 2,
17
+ currentSegmentIndex: 0,
18
+ currentSegment: offer.slices[0].segments[0] as OfferSliceSegment,
19
+ setCurrentSegmentIndex: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function
20
+ },
21
+ };
@@ -0,0 +1,60 @@
1
+ import { Button, ButtonProps } from "@components/Button";
2
+ import type { Meta, StoryFn, StoryObj } from "@storybook/react";
3
+
4
+ export default {
5
+ title: "Button",
6
+ component: Button,
7
+ } as Meta;
8
+
9
+ type ButtonStory = StoryObj<typeof Button>;
10
+
11
+ const defaultProps: ButtonProps = {
12
+ children: "Click me",
13
+ onClick: () => 0,
14
+ };
15
+
16
+ export const Default: ButtonStory = { args: defaultProps };
17
+
18
+ export const Disabled: ButtonStory = {
19
+ args: { ...defaultProps, disabled: true },
20
+ };
21
+
22
+ export const WithIconBefore: ButtonStory = {
23
+ args: { ...defaultProps, iconBefore: "autorenew" },
24
+ };
25
+
26
+ export const WithOutlinedVariant: ButtonStory = {
27
+ args: { ...defaultProps, variant: "outlined" },
28
+ };
29
+
30
+ export const DisabledWithOutlinedVariant: ButtonStory = {
31
+ args: { ...defaultProps, variant: "outlined", disabled: true },
32
+ };
33
+
34
+ export const WithDestructiveVariant: ButtonStory = {
35
+ args: {
36
+ ...defaultProps,
37
+ children: "Click to delete something important",
38
+ variant: "destructive",
39
+ },
40
+ };
41
+
42
+ export const WithSize32: ButtonStory = {
43
+ args: { ...defaultProps, size: 32, iconBefore: "autorenew" },
44
+ };
45
+
46
+ export const WithSize48: ButtonStory = {
47
+ args: { ...defaultProps, size: 48, iconBefore: "autorenew" },
48
+ };
49
+
50
+ export const WithAccentColorSet: StoryFn<ButtonProps> = () => (
51
+ <div
52
+ style={
53
+ {
54
+ "--ACCENT": "29, 78, 216",
55
+ } as any
56
+ }
57
+ >
58
+ <Button {...defaultProps} />
59
+ </div>
60
+ );