@duffel/components 3.0.0-canary → 3.0.0

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 (164) hide show
  1. package/.eslintrc.js +14 -1
  2. package/.github/workflows/release.yml +4 -1
  3. package/.storybook/__snapshots__/Storyshots.test.js.snap +185 -0
  4. package/CONTRIBUTING.md +83 -0
  5. package/README.md +34 -109
  6. package/config/esbuild.base.config.js +7 -3
  7. package/config/esbuild.cdn.config.js +4 -3
  8. package/config/esbuild.dev.config.js +4 -3
  9. package/config/esbuild.react.config.js +3 -3
  10. package/package.json +16 -5
  11. package/react-dist/components/{Card.d.ts → DuffelAncillaries/Card.d.ts} +1 -1
  12. package/react-dist/components/{DuffelAncillaries.d.ts → DuffelAncillaries/DuffelAncillaries.d.ts} +1 -1
  13. package/react-dist/components/{DuffelAncillariesCustomElement.d.ts → DuffelAncillaries/DuffelAncillariesCustomElement.d.ts} +1 -1
  14. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionCard.d.ts +2 -2
  15. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionController.d.ts +2 -2
  16. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModal.d.ts +2 -2
  17. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBody.d.ts +2 -2
  18. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBodyPassenger.d.ts +2 -2
  19. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalFooter.d.ts +1 -1
  20. package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalHeader.d.ts +1 -1
  21. package/react-dist/components/{bags → DuffelAncillaries/bags}/IncludedBaggageBanner.d.ts +1 -1
  22. package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionCard.d.ts +2 -2
  23. package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModal.d.ts +2 -2
  24. package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBody.d.ts +1 -1
  25. package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalFooter.d.ts +2 -2
  26. package/react-dist/components/{seats → DuffelAncillaries/seats}/Amenity.d.ts +1 -1
  27. package/react-dist/components/{seats → DuffelAncillaries/seats}/Element.d.ts +2 -2
  28. package/react-dist/components/{seats → DuffelAncillaries/seats}/Legend.d.ts +1 -1
  29. package/react-dist/components/{seats → DuffelAncillaries/seats}/Row.d.ts +2 -2
  30. package/react-dist/components/{seats → DuffelAncillaries/seats}/RowSection.d.ts +2 -2
  31. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatElement.d.ts +2 -2
  32. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatInfo.d.ts +1 -1
  33. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatMap.d.ts +2 -2
  34. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionCard.d.ts +3 -3
  35. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModal.d.ts +3 -3
  36. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalFooter.d.ts +2 -2
  37. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalHeader.d.ts +1 -1
  38. package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatUnavailable.d.ts +1 -1
  39. package/react-dist/components/DuffelPayments/DuffelPayments.d.ts +11 -0
  40. package/react-dist/components/DuffelPayments/DuffelPaymentsCustomElement.d.ts +14 -0
  41. package/react-dist/components/{Button.d.ts → shared/Button.d.ts} +2 -2
  42. package/react-dist/components/{ErrorBoundary.d.ts → shared/ErrorBoundary.d.ts} +1 -1
  43. package/react-dist/components/{IconButton.d.ts → shared/IconButton.d.ts} +1 -1
  44. package/react-dist/components/{NonIdealState.d.ts → shared/NonIdealState.d.ts} +1 -1
  45. package/react-dist/custom-elements.d.ts +6 -0
  46. package/react-dist/custom-elements.js +37 -0
  47. package/react-dist/custom-elements.js.map +7 -0
  48. package/react-dist/index.d.ts +4 -5
  49. package/react-dist/index.js +21 -20
  50. package/react-dist/index.js.map +4 -4
  51. package/react-dist/lib/captureErrorInSentry.d.ts +1 -1
  52. package/react-dist/lib/fetchFromDuffelAPI.d.ts +7 -0
  53. package/react-dist/lib/logging.d.ts +7 -14
  54. package/react-dist/lib/retrieveSeatMaps.d.ts +1 -1
  55. package/react-dist/types/DuffelAncillariesProps.d.ts +1 -1
  56. package/scripts/generate-fixture.ts +13 -8
  57. package/scripts/upload-to-cdn.sh +4 -9
  58. package/src/components/{Card.tsx → DuffelAncillaries/Card.tsx} +1 -1
  59. package/src/components/{Counter.tsx → DuffelAncillaries/Counter.tsx} +1 -1
  60. package/src/components/{DuffelAncillaries.tsx → DuffelAncillaries/DuffelAncillaries.tsx} +61 -63
  61. package/src/components/{DuffelAncillariesCustomElement.tsx → DuffelAncillaries/DuffelAncillariesCustomElement.tsx} +2 -2
  62. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionCard.tsx +4 -4
  63. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionController.tsx +2 -2
  64. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModal.tsx +3 -3
  65. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBody.tsx +3 -3
  66. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBodyPassenger.tsx +2 -2
  67. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalFooter.tsx +2 -2
  68. package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalHeader.tsx +1 -1
  69. package/src/components/{bags → DuffelAncillaries/bags}/IncludedBaggageBanner.tsx +1 -1
  70. package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionCard.tsx +4 -4
  71. package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModal.tsx +3 -3
  72. package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBody.tsx +3 -3
  73. package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBodyListItem.tsx +1 -1
  74. package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalFooter.tsx +4 -4
  75. package/src/components/{seats → DuffelAncillaries/seats}/Amenity.tsx +2 -2
  76. package/src/components/{seats → DuffelAncillaries/seats}/DeckSelect.tsx +1 -1
  77. package/src/components/{seats → DuffelAncillaries/seats}/Element.tsx +2 -2
  78. package/src/components/{seats → DuffelAncillaries/seats}/ExitElement.tsx +1 -1
  79. package/src/components/{seats → DuffelAncillaries/seats}/Legend.tsx +2 -2
  80. package/src/components/{seats → DuffelAncillaries/seats}/Row.tsx +2 -2
  81. package/src/components/{seats → DuffelAncillaries/seats}/RowSection.tsx +5 -2
  82. package/src/components/{seats → DuffelAncillaries/seats}/SeatElement.tsx +3 -3
  83. package/src/components/{seats → DuffelAncillaries/seats}/SeatInfo.tsx +1 -1
  84. package/src/components/{seats → DuffelAncillaries/seats}/SeatMap.tsx +2 -2
  85. package/src/components/{seats → DuffelAncillaries/seats}/SeatMapUnavailable.tsx +1 -1
  86. package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionCard.tsx +5 -5
  87. package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModal.tsx +4 -4
  88. package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalBody.tsx +1 -1
  89. package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalFooter.tsx +3 -3
  90. package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalHeader.tsx +1 -1
  91. package/src/components/{seats → DuffelAncillaries/seats}/SeatUnavailable.tsx +2 -2
  92. package/src/components/DuffelPayments/DuffelPayments.tsx +218 -0
  93. package/src/components/DuffelPayments/DuffelPaymentsCustomElement.tsx +130 -0
  94. package/src/components/{Button.tsx → shared/Button.tsx} +4 -3
  95. package/src/components/{ErrorBoundary.tsx → shared/ErrorBoundary.tsx} +2 -2
  96. package/src/components/{Icon.tsx → shared/Icon.tsx} +2 -1
  97. package/src/components/{IconButton.tsx → shared/IconButton.tsx} +1 -1
  98. package/src/components/{Modal.tsx → shared/Modal.tsx} +5 -1
  99. package/src/components/{NonIdealState.tsx → shared/NonIdealState.tsx} +1 -1
  100. package/src/custom-elements.ts +13 -0
  101. package/src/examples/client-side/README.md +30 -0
  102. package/src/examples/client-side/index.html +1 -1
  103. package/src/examples/full-stack/README.md +34 -0
  104. package/src/examples/full-stack/index.html +1 -1
  105. package/src/examples/full-stack/server.mjs +1 -0
  106. package/src/examples/just-typescript/src/index.html +2 -2
  107. package/src/examples/just-typescript/src/index.ts +2 -1
  108. package/src/examples/payments-custom-element/README.md +17 -0
  109. package/src/examples/payments-custom-element/index.html +43 -0
  110. package/src/examples/payments-just-typescript/README.md +37 -0
  111. package/src/examples/payments-just-typescript/package.json +16 -0
  112. package/src/examples/payments-just-typescript/src/index.html +23 -0
  113. package/src/examples/payments-just-typescript/src/index.ts +18 -0
  114. package/src/examples/payments-just-typescript/yarn.lock +154 -0
  115. package/src/examples/react-app/src/index.tsx +11 -6
  116. package/src/index.ts +4 -5
  117. package/src/lib/captureErrorInSentry.ts +2 -20
  118. package/src/lib/fetchFromDuffelAPI.ts +36 -6
  119. package/src/lib/formatDate.ts +3 -4
  120. package/src/lib/getBaggageServiceDescription.ts +2 -4
  121. package/src/lib/getTotalAmountForServices.ts +1 -1
  122. package/src/lib/logging.ts +52 -32
  123. package/src/lib/retrieveOffer.ts +11 -6
  124. package/src/lib/retrieveSeatMaps.ts +13 -8
  125. package/src/stories/BaggageSelectionModalHeader.stories.tsx +1 -1
  126. package/src/stories/Button.stories.tsx +12 -3
  127. package/src/stories/DuffelAncillaries.stories.tsx +1 -1
  128. package/src/stories/DuffelPayments.stories.tsx +34 -0
  129. package/src/stories/Icon.stories.tsx +3 -2
  130. package/src/stories/IconButton.stories.tsx +1 -1
  131. package/src/styles/components/DuffelPayments.css +42 -0
  132. package/src/styles/components/Modal.css +2 -1
  133. package/src/styles/global.css +1 -0
  134. package/src/tests/components/DuffelAncillaries.test.tsx +1 -1
  135. package/src/tests/lib/createPriceFormatters.test.tsx +1 -1
  136. package/src/tests/lib/formatAvailableServices.test.tsx +1 -1
  137. package/src/tests/lib/formatSeatMaps.test.tsx +2 -2
  138. package/src/tests/lib/getCurrencyForServices.test.tsx +1 -1
  139. package/src/tests/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.test.ts +1 -1
  140. package/src/tests/lib/logging.test.tsx +14 -14
  141. package/src/tests/lib/moneyStringFormatter.test.tsx +1 -1
  142. package/src/tests/lib/validateProps.test.tsx +1 -1
  143. package/src/types/DuffelAncillariesProps.ts +1 -1
  144. package/tsconfig.json +1 -1
  145. /package/react-dist/components/{Counter.d.ts → DuffelAncillaries/Counter.d.ts} +0 -0
  146. /package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBodyListItem.d.ts +0 -0
  147. /package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalHeader.d.ts +0 -0
  148. /package/react-dist/components/{seats → DuffelAncillaries/seats}/DeckSelect.d.ts +0 -0
  149. /package/react-dist/components/{seats → DuffelAncillaries/seats}/EmptyElement.d.ts +0 -0
  150. /package/react-dist/components/{seats → DuffelAncillaries/seats}/ExitElement.d.ts +0 -0
  151. /package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatMapUnavailable.d.ts +0 -0
  152. /package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalBody.d.ts +0 -0
  153. /package/react-dist/components/{AnimatedLoaderEllipsis.d.ts → shared/AnimatedLoaderEllipsis.d.ts} +0 -0
  154. /package/react-dist/components/{FetchOfferErrorState.d.ts → shared/FetchOfferErrorState.d.ts} +0 -0
  155. /package/react-dist/components/{Icon.d.ts → shared/Icon.d.ts} +0 -0
  156. /package/react-dist/components/{Modal.d.ts → shared/Modal.d.ts} +0 -0
  157. /package/react-dist/components/{Stamp.d.ts → shared/Stamp.d.ts} +0 -0
  158. /package/react-dist/components/{Tabs.d.ts → shared/Tabs.d.ts} +0 -0
  159. /package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalHeader.tsx +0 -0
  160. /package/src/components/{seats → DuffelAncillaries/seats}/EmptyElement.tsx +0 -0
  161. /package/src/components/{AnimatedLoaderEllipsis.tsx → shared/AnimatedLoaderEllipsis.tsx} +0 -0
  162. /package/src/components/{FetchOfferErrorState.tsx → shared/FetchOfferErrorState.tsx} +0 -0
  163. /package/src/components/{Stamp.tsx → shared/Stamp.tsx} +0 -0
  164. /package/src/components/{Tabs.tsx → shared/Tabs.tsx} +0 -0
@@ -1,7 +1,10 @@
1
1
  import classNames from "classnames";
2
2
  import * as React from "react";
3
- import { CreateOrderPayloadSeatService } from "../../types/CreateOrderPayload";
4
- import { SeatMapCabinRow, SeatMapCabinRowSection } from "../../types/SeatMap";
3
+ import { CreateOrderPayloadSeatService } from "../../../types/CreateOrderPayload";
4
+ import {
5
+ SeatMapCabinRow,
6
+ SeatMapCabinRowSection,
7
+ } from "../../../types/SeatMap";
5
8
  import { Element } from "./Element";
6
9
  import { EmptyElement } from "./EmptyElement";
7
10
 
@@ -1,10 +1,10 @@
1
+ import { Icon } from "@components/shared/Icon";
1
2
  import { getPassengerInitials } from "@lib/getPassengerInitials";
2
3
  import { moneyStringFormatter } from "@lib/moneyStringFormatter";
3
4
  import classNames from "classnames";
4
5
  import * as React from "react";
5
- import { CreateOrderPayloadSeatService } from "../../types/CreateOrderPayload";
6
- import { SeatMapCabinRowSectionElementSeat } from "../../types/SeatMap";
7
- import { Icon } from "../Icon";
6
+ import { CreateOrderPayloadSeatService } from "../../../types/CreateOrderPayload";
7
+ import { SeatMapCabinRowSectionElementSeat } from "../../../types/SeatMap";
8
8
  import { SeatInfo } from "./SeatInfo";
9
9
  import { SeatUnavailable } from "./SeatUnavailable";
10
10
 
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import {
4
4
  SeatMapCabinRowSectionAvailableService,
5
5
  SeatMapCabinRowSectionElementSeat,
6
- } from "../../types/SeatMap";
6
+ } from "../../../types/SeatMap";
7
7
 
8
8
  export interface SeatInfoProps {
9
9
  seat: SeatMapCabinRowSectionElementSeat | null;
@@ -3,8 +3,8 @@ import { getSymbols } from "@lib/getSymbols";
3
3
  import { hasWings } from "@lib/hasWings";
4
4
  import classNames from "classnames";
5
5
  import * as React from "react";
6
- import { CreateOrderPayloadSeatService } from "../../types/CreateOrderPayload";
7
- import { SeatMap as SeatMapType } from "../../types/SeatMap";
6
+ import { CreateOrderPayloadSeatService } from "../../../types/CreateOrderPayload";
7
+ import { SeatMap as SeatMapType } from "../../../types/SeatMap";
8
8
  import { DeckSelect } from "./DeckSelect";
9
9
  import { Legend } from "./Legend";
10
10
  import { Row } from "./Row";
@@ -1,4 +1,4 @@
1
- import { NonIdealState } from "@components/NonIdealState";
1
+ import { NonIdealState } from "@components/shared/NonIdealState";
2
2
  import * as React from "react";
3
3
 
4
4
  export const SeatMapUnavailable: React.FC = () => (
@@ -1,3 +1,5 @@
1
+ import { AnimatedLoaderEllipsis } from "@components/shared/AnimatedLoaderEllipsis";
2
+ import { Stamp } from "@components/shared/Stamp";
1
3
  import { getCurrencyForSeatMaps } from "@lib/getCurrencyForSeatMaps";
2
4
  import { getTotalAmountForServices } from "@lib/getTotalAmountForServices";
3
5
  import { getTotalQuantity } from "@lib/getTotalQuantity";
@@ -8,12 +10,10 @@ import {
8
10
  CreateOrderPayload,
9
11
  CreateOrderPayloadSeatService,
10
12
  CreateOrderPayloadServices,
11
- } from "../../types/CreateOrderPayload";
12
- import { Offer } from "../../types/Offer";
13
- import { SeatMap } from "../../types/SeatMap";
14
- import { AnimatedLoaderEllipsis } from "../AnimatedLoaderEllipsis";
13
+ } from "../../../types/CreateOrderPayload";
14
+ import { Offer } from "../../../types/Offer";
15
+ import { SeatMap } from "../../../types/SeatMap";
15
16
  import { Card } from "../Card";
16
- import { Stamp } from "../Stamp";
17
17
  import { SeatSelectionModal } from "./SeatSelectionModal";
18
18
 
19
19
  export interface SeatSelectionCardProps {
@@ -1,3 +1,4 @@
1
+ import { Modal } from "@components/shared/Modal";
1
2
  import { getCurrencyForSeatMaps } from "@lib/getCurrencyForSeatMaps";
2
3
  import { getPassengerBySegmentList } from "@lib/getPassengerBySegmentList";
3
4
  import { getPassengerMapById } from "@lib/getPassengerMapById";
@@ -8,10 +9,9 @@ import React from "react";
8
9
  import {
9
10
  CreateOrderPayload,
10
11
  CreateOrderPayloadSeatService,
11
- } from "../../types/CreateOrderPayload";
12
- import { Offer } from "../../types/Offer";
13
- import { SeatMap } from "../../types/SeatMap";
14
- import { Modal } from "../Modal";
12
+ } from "../../../types/CreateOrderPayload";
13
+ import { Offer } from "../../../types/Offer";
14
+ import { SeatMap } from "../../../types/SeatMap";
15
15
  import { SeatSelectionModalBody } from "./SeatSelectionModalBody";
16
16
  import { SeatSelectionModalFooter } from "./SeatSelectionModalFooter";
17
17
  import { SeatSelectionModalHeader } from "./SeatSelectionModalHeader";
@@ -1,4 +1,4 @@
1
- import { ModalBody } from "@components/Modal";
1
+ import { ModalBody } from "@components/shared/Modal";
2
2
  import React from "react";
3
3
  import { SeatMap, SeatMapProps } from "./SeatMap";
4
4
 
@@ -1,12 +1,12 @@
1
+ import { Button } from "@components/shared/Button";
1
2
  import { ServicePriceMapById } from "@lib/getServicePriceMapById";
2
3
  import { getTotalAmountForServicesWithPriceMap } from "@lib/getTotalAmountForServices";
3
4
  import { getTotalQuantity } from "@lib/getTotalQuantity";
4
5
  import { moneyStringFormatter } from "@lib/moneyStringFormatter";
5
6
  import { withPlural } from "@lib/withPlural";
6
7
  import React from "react";
7
- import { CreateOrderPayloadServices } from "../../types/CreateOrderPayload";
8
- import { SeatMap } from "../../types/SeatMap";
9
- import { Button } from "../Button";
8
+ import { CreateOrderPayloadServices } from "../../../types/CreateOrderPayload";
9
+ import { SeatMap } from "../../../types/SeatMap";
10
10
 
11
11
  export interface SeatSelectionModalFooterProps {
12
12
  currency: string;
@@ -1,6 +1,6 @@
1
1
  import { formatDateString } from "@lib/formatDate";
2
2
  import React from "react";
3
- import { OfferSliceSegment } from "../../types/Offer";
3
+ import { OfferSliceSegment } from "../../../types/Offer";
4
4
 
5
5
  export interface SeatSelectionModalHeaderProps {
6
6
  segmentAndPassengerPermutationsCount: number;
@@ -1,6 +1,6 @@
1
+ import { Icon } from "@components/shared/Icon";
1
2
  import * as React from "react";
2
- import { SeatMapCabinRowSectionElementSeat } from "../../types/SeatMap";
3
- import { Icon } from "../Icon";
3
+ import { SeatMapCabinRowSectionElementSeat } from "../../../types/SeatMap";
4
4
 
5
5
  export const SeatUnavailable: React.FC<{
6
6
  seat: SeatMapCabinRowSectionElementSeat;
@@ -0,0 +1,218 @@
1
+ import { ErrorBoundary } from "@components/shared/ErrorBoundary";
2
+ import { initializeLogger } from "@lib/logging";
3
+ import {
4
+ CardElement,
5
+ Elements,
6
+ useElements,
7
+ useStripe,
8
+ } from "@stripe/react-stripe-js";
9
+ import { StripeCardElement, StripeError, loadStripe } from "@stripe/stripe-js";
10
+ import * as React from "react";
11
+ import { CustomStyles } from "../../types";
12
+ import { Button } from "../shared/Button";
13
+
14
+ const COMPONENT_CDN = process.env.COMPONENT_CDN || "";
15
+ const hrefToComponentStyles = `${COMPONENT_CDN}/global.css`;
16
+
17
+ const STRIPE_CARD_ELEMENT = "card";
18
+ const COMPONENT_VERSION = process.env.COMPONENT_VERSION;
19
+
20
+ export interface DuffelPaymentsProps {
21
+ paymentIntentClientToken: string;
22
+ onSuccessfulPayment: () => void;
23
+ onFailedPayment: (error: StripeError) => void;
24
+
25
+ styles?: CustomStyles;
26
+ debug?: boolean;
27
+ }
28
+
29
+ const CARD_ELEMENT_STYLE_BASE = {
30
+ color: "var(--GREY-900)",
31
+ fontFamily:
32
+ '"-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "sans-serif"',
33
+ fontSmoothing: "antialiased",
34
+ fontSize: "16px",
35
+ "::placeholder": {
36
+ color: "#ababb4",
37
+ },
38
+ };
39
+
40
+ const CARD_ELEMENT_STYLE_INVALID = {
41
+ color: "#ef4444",
42
+ iconColor: "#ef4444",
43
+ };
44
+
45
+ const CardPaymentComponent: React.FC<DuffelPaymentsProps> = ({
46
+ styles,
47
+ paymentIntentClientToken,
48
+ onSuccessfulPayment,
49
+ onFailedPayment,
50
+ }) => {
51
+ const [cardElement, setCardElement] =
52
+ React.useState<StripeCardElement | null>(null);
53
+ const [isComplete, setIsComplete] = React.useState<boolean>(false);
54
+ const [isInvalid, setIsInvalid] = React.useState<boolean>(false);
55
+ const [isProcessing, setIsProcessing] = React.useState<boolean>(false);
56
+ const [errorMessage, setErrorMessage] = React.useState<string>("");
57
+ const stripe = useStripe();
58
+ const elements = useElements();
59
+
60
+ React.useEffect(() => {
61
+ if (elements && !cardElement) {
62
+ const maybeCard = elements?.getElement(STRIPE_CARD_ELEMENT);
63
+ maybeCard && setCardElement(maybeCard as StripeCardElement);
64
+ }
65
+ }, [elements, cardElement]);
66
+
67
+ React.useEffect(() => {
68
+ if (cardElement) {
69
+ cardElement.on("change", (event) => {
70
+ const { error, complete } = event;
71
+
72
+ if (error) {
73
+ setIsInvalid(true);
74
+ setErrorMessage(error.message);
75
+ } else {
76
+ setIsInvalid(false);
77
+ }
78
+ complete ? setIsComplete(true) : setIsComplete(false);
79
+ });
80
+ }
81
+ }, [cardElement]);
82
+
83
+ // User is responsible for handling outcome of payment.
84
+ const handleSubmit = async (
85
+ e: React.MouseEvent<HTMLFormElement, MouseEvent>
86
+ ) => {
87
+ e.preventDefault();
88
+ if (!stripe || !elements) {
89
+ return;
90
+ }
91
+
92
+ if (cardElement) {
93
+ setIsProcessing(true);
94
+ const decodedDuffelPaymentIntentClientToken = atob(
95
+ paymentIntentClientToken
96
+ );
97
+ const parsedDuffelPaymentIntentClientToken = JSON.parse(
98
+ decodedDuffelPaymentIntentClientToken
99
+ );
100
+ const { client_secret: clientSecret } =
101
+ parsedDuffelPaymentIntentClientToken;
102
+
103
+ const result = await stripe.confirmCardPayment(clientSecret, {
104
+ payment_method: {
105
+ card: cardElement,
106
+ metadata: {
107
+ duffel_components_version:
108
+ COMPONENT_VERSION || "failed-to-get-version",
109
+ },
110
+ },
111
+ });
112
+
113
+ setIsProcessing(false);
114
+ if (result.error) {
115
+ const { error } = result;
116
+ onFailedPayment(error);
117
+ } else {
118
+ onSuccessfulPayment();
119
+ }
120
+ }
121
+ };
122
+
123
+ return paymentIntentClientToken ? (
124
+ <div className="card-payment__container">
125
+ <form onSubmit={handleSubmit}>
126
+ {isProcessing && (
127
+ <div
128
+ className="card-payment--in-progress"
129
+ aria-live="polite"
130
+ aria-busy="true"
131
+ />
132
+ )}
133
+ <CardElement
134
+ className="card-details"
135
+ options={{
136
+ style: {
137
+ base: {
138
+ ...CARD_ELEMENT_STYLE_BASE,
139
+ ...(styles?.fontFamily && {
140
+ fontFamily: styles.fontFamily,
141
+ }),
142
+ },
143
+ invalid: { ...CARD_ELEMENT_STYLE_INVALID },
144
+ },
145
+ }}
146
+ />
147
+ <div className="card-payment__container--invalid" role="alert">
148
+ {isInvalid && errorMessage}
149
+ </div>
150
+ <Button
151
+ className="card-payment__pay-button"
152
+ type="submit"
153
+ disabled={!isComplete || isProcessing}
154
+ aria-label="Pay"
155
+ >
156
+ {isProcessing ? "Processing..." : "Pay"}
157
+ </Button>
158
+ </form>
159
+ </div>
160
+ ) : null;
161
+ };
162
+
163
+ export const DuffelPayments: React.FC<DuffelPaymentsProps> = (props) => {
164
+ const [stripe, setStripe] = React.useState<Promise<any> | null>(null);
165
+ initializeLogger(props.debug || false);
166
+
167
+ const decodedDuffelPaymentIntentClientToken = atob(
168
+ props.paymentIntentClientToken
169
+ );
170
+
171
+ let parsedDuffelPaymentIntentClientToken;
172
+
173
+ try {
174
+ parsedDuffelPaymentIntentClientToken = JSON.parse(
175
+ decodedDuffelPaymentIntentClientToken
176
+ );
177
+ } catch (error) {
178
+ throw new Error("Invalid Duffel payment intent client token provided");
179
+ }
180
+
181
+ const { publishable_key: duffelPublishableKey } =
182
+ parsedDuffelPaymentIntentClientToken;
183
+
184
+ React.useEffect(() => {
185
+ if (!stripe && duffelPublishableKey) {
186
+ const stripe = loadStripe(duffelPublishableKey);
187
+ stripe && setStripe(stripe);
188
+ }
189
+ }, [stripe, duffelPublishableKey]);
190
+
191
+ const duffelComponentsStyle: React.CSSProperties = {
192
+ ...(props.styles?.accentColor && {
193
+ "--ACCENT": props.styles.accentColor,
194
+ }),
195
+ ...(props.styles?.fontFamily && {
196
+ "--FONT-FAMILY": props.styles.fontFamily,
197
+ }),
198
+ ...(props.styles?.buttonCornerRadius && {
199
+ "--BUTTON-RADIUS": props.styles.buttonCornerRadius,
200
+ }),
201
+ // `as any` is needed here is needed because we want to set css variables
202
+ // that are not part of the css properties type
203
+ } as any;
204
+
205
+ return (
206
+ <>
207
+ <link rel="stylesheet" href={hrefToComponentStyles}></link>
208
+
209
+ <div className="duffel-components" style={duffelComponentsStyle}>
210
+ <ErrorBoundary>
211
+ <Elements stripe={stripe}>
212
+ <CardPaymentComponent {...props} />
213
+ </Elements>
214
+ </ErrorBoundary>
215
+ </div>
216
+ </>
217
+ );
218
+ };
@@ -0,0 +1,130 @@
1
+ import { StripeError } from "@stripe/stripe-js";
2
+ import { createRoot, Root } from "react-dom/client";
3
+ import { DuffelPayments, DuffelPaymentsProps } from "./DuffelPayments";
4
+
5
+ declare global {
6
+ // eslint-disable-next-line @typescript-eslint/no-namespace
7
+ namespace JSX {
8
+ interface IntrinsicElements {
9
+ "duffel-payments": React.DetailedHTMLProps<
10
+ React.HTMLAttributes<HTMLElement>,
11
+ HTMLElement
12
+ >;
13
+ }
14
+ }
15
+ }
16
+
17
+ const CUSTOM_ELEMENT_TAG = "duffel-payments";
18
+
19
+ type DuffelPaymentsCustomElementRenderArguments = Pick<
20
+ DuffelPaymentsProps,
21
+ "paymentIntentClientToken" | "styles"
22
+ >;
23
+
24
+ class DuffelPaymentsCustomElement extends HTMLElement {
25
+ /**
26
+ * The React root for displaying content inside a browser DOM element.
27
+ */
28
+ private root!: Root;
29
+
30
+ /**
31
+ * `connectedCallback` is called to initialise the custom element
32
+ */
33
+ connectedCallback() {
34
+ const container = document.createElement("div");
35
+ this.appendChild(container);
36
+
37
+ this.root = createRoot(container);
38
+ }
39
+
40
+ /**
41
+ * When this function is called, it will render/re-render
42
+ * the `DuffelPayments` component with the given props.
43
+ */
44
+ public render(withProps: DuffelPaymentsCustomElementRenderArguments) {
45
+ if (!this.root) {
46
+ throw "It was not possible to render `duffel-payments` because `this.root` is missing.";
47
+ }
48
+
49
+ this.root.render(
50
+ <DuffelPayments
51
+ {...withProps}
52
+ onSuccessfulPayment={() => {
53
+ this.dispatchEvent(
54
+ new CustomEvent("onSuccessfulPayment", {
55
+ composed: true,
56
+ })
57
+ );
58
+ }}
59
+ onFailedPayment={(error: StripeError) => {
60
+ this.dispatchEvent(
61
+ new CustomEvent("onFailedPayment", {
62
+ detail: { error },
63
+ composed: true,
64
+ })
65
+ );
66
+ }}
67
+ />
68
+ );
69
+ }
70
+ }
71
+
72
+ window.customElements.get(CUSTOM_ELEMENT_TAG) ||
73
+ window.customElements.define(CUSTOM_ELEMENT_TAG, DuffelPaymentsCustomElement);
74
+
75
+ function tryToGetDuffelPaymentsCustomElement(
76
+ caller: string
77
+ ): DuffelPaymentsCustomElement {
78
+ const element =
79
+ document.querySelector<DuffelPaymentsCustomElement>(CUSTOM_ELEMENT_TAG);
80
+ if (!element) {
81
+ throw new Error(
82
+ `Could not find duffel-payments element in the DOM. Maybe you need to call ${caller} after 'window.onload'?`
83
+ );
84
+ }
85
+ return element;
86
+ }
87
+
88
+ export function renderDuffelPaymentsCustomElement(
89
+ props: DuffelPaymentsCustomElementRenderArguments
90
+ ) {
91
+ const element = tryToGetDuffelPaymentsCustomElement(
92
+ "renderDuffelPaymentsCustomElement"
93
+ );
94
+ element.render(props);
95
+ }
96
+
97
+ export function onDuffelPaymentsSuccessfulPayment(
98
+ onSuccessfulPayment: DuffelPaymentsProps["onSuccessfulPayment"]
99
+ ) {
100
+ const element = tryToGetDuffelPaymentsCustomElement(
101
+ "onDuffelPaymentsPayloadReady"
102
+ );
103
+
104
+ // using `as EventListener` here because typescript doesn't know the event type for `onPayloadReady`
105
+ // There's a few different suggestions to resolve this seemed good enough
106
+ // You can learn more here: https://github.com/microsoft/TypeScript/issues/28357
107
+ element.addEventListener(
108
+ "onPayloadReady",
109
+ onSuccessfulPayment as EventListener
110
+ );
111
+ }
112
+
113
+ type OnFailedPaymentCustomEvent = CustomEvent<{
114
+ error: StripeError;
115
+ }>;
116
+ export function onDuffelPaymentsFailedPayment(
117
+ onFailedPayment: DuffelPaymentsProps["onFailedPayment"]
118
+ ) {
119
+ const element = tryToGetDuffelPaymentsCustomElement(
120
+ "onDuffelPaymentsPayloadReady"
121
+ );
122
+ const eventListener = (event: OnFailedPaymentCustomEvent) => {
123
+ onFailedPayment(event.detail.error);
124
+ };
125
+
126
+ // using `as EventListener` here because typescript doesn't know the event type for `onPayloadReady`
127
+ // There's a few different suggestions to resolve this seemed good enough
128
+ // You can learn more here: https://github.com/microsoft/TypeScript/issues/28357
129
+ element.addEventListener("onPayloadReady", eventListener as EventListener);
130
+ }
@@ -1,4 +1,4 @@
1
- import { Icon, IconName } from "@components/Icon";
1
+ import { Icon, IconName } from "@components/shared/Icon";
2
2
  import classNames from "classnames";
3
3
  import * as React from "react";
4
4
 
@@ -25,7 +25,7 @@ type NativeButtonProps = React.DetailedHTMLProps<
25
25
  export interface ButtonProps
26
26
  extends Pick<
27
27
  NativeButtonProps,
28
- "id" | "onClick" | "disabled" | "children" | "className"
28
+ "id" | "onClick" | "disabled" | "children" | "className" | "type"
29
29
  > {
30
30
  "data-testid"?: string;
31
31
  iconBefore?: IconName;
@@ -39,10 +39,11 @@ export const Button: React.FC<ButtonProps> = ({
39
39
  size = 40,
40
40
  children,
41
41
  className,
42
+ type = "button",
42
43
  ...nativeButtonProps
43
44
  }) => (
44
45
  <button
45
- type="button"
46
+ type={type}
46
47
  className={classNames(
47
48
  "button",
48
49
  BUTTON_VARIANTS[variant],
@@ -13,9 +13,9 @@ export class ErrorBoundary extends React.Component<{
13
13
  return { hasError: true };
14
14
  }
15
15
 
16
- componentDidCatch(error: Error, context: Record<any, any>) {
16
+ componentDidCatch(error: Error) {
17
17
  // You can also log the error to an error reporting service
18
- captureErrorInSentry(error, context);
18
+ captureErrorInSentry(error);
19
19
  }
20
20
 
21
21
  render() {
@@ -1,3 +1,4 @@
1
+ import { log } from "@lib/logging";
1
2
  import * as React from "react";
2
3
 
3
4
  /* eslint-disable react/no-unknown-property */
@@ -109,7 +110,7 @@ export type IconName = keyof typeof ICON_MAP;
109
110
 
110
111
  const getIconPath = (name: IconName) => {
111
112
  if (!(name in ICON_MAP)) {
112
- console.warn(`The icon "${name}" is missing from ICON_MAP`);
113
+ log(`The icon "${name}" is missing from ICON_MAP`);
113
114
  return null;
114
115
  }
115
116
  return ICON_MAP[name];
@@ -1,6 +1,6 @@
1
- import { Icon, IconName } from "@components/Icon";
2
1
  import classNames from "classnames";
3
2
  import * as React from "react";
3
+ import { Icon, IconName } from "./Icon";
4
4
 
5
5
  const ICON_BUTTON_VARIANTS = {
6
6
  primary: "icon-button--primary",
@@ -16,7 +16,11 @@ export const Modal: React.FC<ModalProps> = ({ children, onClose, isOpen }) => {
16
16
  }, [isOpen]);
17
17
 
18
18
  return (
19
- <div className={classNames("modal", isOpen && "modal--open")}>
19
+ <div
20
+ className={classNames("modal", isOpen && "modal--open")}
21
+ // setting inline style to avoid modal content to flash unstyled before stylesheet is loaded
22
+ style={{ opacity: 0 }}
23
+ >
20
24
  <div role="presentation" className={"modal--content"}>
21
25
  {children}
22
26
 
@@ -1,4 +1,4 @@
1
- import * as React from "react";
1
+ import React from "react";
2
2
 
3
3
  export const NonIdealState: React.FC<
4
4
  React.PropsWithChildren<{ style?: React.CSSProperties }>
@@ -0,0 +1,13 @@
1
+ /**
2
+ * This file is one of the entry points for the library -- `@duffel/components/custom-elements`.
3
+ * If you'd like to expose other custom element functions, please add them here.
4
+ */
5
+ export {
6
+ onDuffelAncillariesPayloadReady,
7
+ renderDuffelAncillariesCustomElement,
8
+ } from "./components/DuffelAncillaries/DuffelAncillariesCustomElement";
9
+ export {
10
+ onDuffelPaymentsFailedPayment,
11
+ onDuffelPaymentsSuccessfulPayment,
12
+ renderDuffelPaymentsCustomElement,
13
+ } from "./components/DuffelPayments/DuffelPaymentsCustomElement";
@@ -0,0 +1,30 @@
1
+ # duffel-components client-side example
2
+
3
+ ## Setup
4
+
5
+ ```sh
6
+ # .env.local
7
+
8
+ # The Duffel API environment we want to make requests to.
9
+ # Remember to update the key below if this value changes.
10
+ # DUFFEL_API_URL=https://api.duffel.com # production
11
+ DUFFEL_API_URL=https://localhost:4000 # development (for Duffel engineers only)
12
+
13
+ # Used to authenticate our
14
+ # example server to talk to Duffel
15
+ # DUFFEL_API_TOKEN=duffel_test_**** # production
16
+ DUFFEL_API_TOKEN=test_duffel_dev_rw # development (for Duffel engineers only)
17
+
18
+
19
+ # The url for the component CDN.
20
+ # This is used to load both the styles an
21
+ # COMPONENT_CDN=https://assets.duffel.com/components/ancillaries/VERSION # production
22
+ COMPONENT_CDN=http://localhost:8000 # development
23
+ ```
24
+
25
+ ## Run the example
26
+
27
+ This is a basic example that doesn't rely on the Duffel API, from the root of the repository run `yarn run-example:client-side`. This command will:
28
+
29
+ 1. Serve the Duffel component bundle and watch for changes to rebuild on port `8000`. This can also be done with `yarn dev`
30
+ 2. Host a basic `index.html` with `http-server`. The example page will be ready on port `6262`. This can be done with `yarn run-client-side-server`
@@ -11,7 +11,7 @@
11
11
  />
12
12
 
13
13
  <!-- 1. This script loads duffel-components -->
14
- <script src="http://localhost:8000/ancillaries/index.js"></script>
14
+ <script src="http://localhost:8000/duffel-ancillaries.js"></script>
15
15
  </head>
16
16
 
17
17
  <body style="font-family: sans-serif">
@@ -0,0 +1,34 @@
1
+ # duffel-components full-stack example
2
+
3
+ ## Setup
4
+
5
+ ```sh
6
+ # .env.local
7
+
8
+ # The Duffel API environment we want to make requests to.
9
+ # Remember to update the key below if this value changes.
10
+ # DUFFEL_API_URL=https://api.duffel.com # production
11
+ DUFFEL_API_URL=https://localhost:4000 # development (for Duffel engineers only)
12
+
13
+ # Used to authenticate our
14
+ # example server to talk to Duffel
15
+ # DUFFEL_API_TOKEN=duffel_test_**** # production (find it on https://app.duffel.com/YOUR_ORG/test/developers/tokens)
16
+ DUFFEL_API_TOKEN=test_duffel_dev_rw # development (for Duffel engineers only)
17
+
18
+
19
+ # The url for the component CDN.
20
+ # This is used to load both the styles an
21
+ # COMPONENT_CDN=https://assets.duffel.com/components/ancillaries/VERSION # production
22
+ COMPONENT_CDN=http://localhost:8000 # development
23
+ ```
24
+
25
+ ## Run the example
26
+
27
+ This example has a server that will reach out to the Duffel API to search and retrieve an offer.
28
+ To talk to the API we'll define the url to the API environment we want and .
29
+ Please add the following to `.env.local`:
30
+
31
+ This is a 'real life' example, where it uses a real offer ID and client key retrieved from the Duffel API. To run this, use `yarn run-example:full-stack`. This command will:
32
+
33
+ 1. Serve the Duffel component bundle and watch for changes to rebuild on port `8000`. This can also be done with `yarn dev`
34
+ 2. Run the full stack server using node. The example page will be ready on port `6262`. This can be done with `yarn run-full-stack-server`