@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.
- package/.eslintrc.js +14 -1
- package/.github/workflows/release.yml +4 -1
- package/.storybook/__snapshots__/Storyshots.test.js.snap +185 -0
- package/CONTRIBUTING.md +83 -0
- package/README.md +34 -109
- package/config/esbuild.base.config.js +7 -3
- package/config/esbuild.cdn.config.js +4 -3
- package/config/esbuild.dev.config.js +4 -3
- package/config/esbuild.react.config.js +3 -3
- package/package.json +16 -5
- package/react-dist/components/{Card.d.ts → DuffelAncillaries/Card.d.ts} +1 -1
- package/react-dist/components/{DuffelAncillaries.d.ts → DuffelAncillaries/DuffelAncillaries.d.ts} +1 -1
- package/react-dist/components/{DuffelAncillariesCustomElement.d.ts → DuffelAncillaries/DuffelAncillariesCustomElement.d.ts} +1 -1
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionCard.d.ts +2 -2
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionController.d.ts +2 -2
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModal.d.ts +2 -2
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBody.d.ts +2 -2
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBodyPassenger.d.ts +2 -2
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalFooter.d.ts +1 -1
- package/react-dist/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalHeader.d.ts +1 -1
- package/react-dist/components/{bags → DuffelAncillaries/bags}/IncludedBaggageBanner.d.ts +1 -1
- package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionCard.d.ts +2 -2
- package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModal.d.ts +2 -2
- package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBody.d.ts +1 -1
- package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalFooter.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/Amenity.d.ts +1 -1
- package/react-dist/components/{seats → DuffelAncillaries/seats}/Element.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/Legend.d.ts +1 -1
- package/react-dist/components/{seats → DuffelAncillaries/seats}/Row.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/RowSection.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatElement.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatInfo.d.ts +1 -1
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatMap.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionCard.d.ts +3 -3
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModal.d.ts +3 -3
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalFooter.d.ts +2 -2
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalHeader.d.ts +1 -1
- package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatUnavailable.d.ts +1 -1
- package/react-dist/components/DuffelPayments/DuffelPayments.d.ts +11 -0
- package/react-dist/components/DuffelPayments/DuffelPaymentsCustomElement.d.ts +14 -0
- package/react-dist/components/{Button.d.ts → shared/Button.d.ts} +2 -2
- package/react-dist/components/{ErrorBoundary.d.ts → shared/ErrorBoundary.d.ts} +1 -1
- package/react-dist/components/{IconButton.d.ts → shared/IconButton.d.ts} +1 -1
- package/react-dist/components/{NonIdealState.d.ts → shared/NonIdealState.d.ts} +1 -1
- package/react-dist/custom-elements.d.ts +6 -0
- package/react-dist/custom-elements.js +37 -0
- package/react-dist/custom-elements.js.map +7 -0
- package/react-dist/index.d.ts +4 -5
- package/react-dist/index.js +21 -20
- package/react-dist/index.js.map +4 -4
- package/react-dist/lib/captureErrorInSentry.d.ts +1 -1
- package/react-dist/lib/fetchFromDuffelAPI.d.ts +7 -0
- package/react-dist/lib/logging.d.ts +7 -14
- package/react-dist/lib/retrieveSeatMaps.d.ts +1 -1
- package/react-dist/types/DuffelAncillariesProps.d.ts +1 -1
- package/scripts/generate-fixture.ts +13 -8
- package/scripts/upload-to-cdn.sh +4 -9
- package/src/components/{Card.tsx → DuffelAncillaries/Card.tsx} +1 -1
- package/src/components/{Counter.tsx → DuffelAncillaries/Counter.tsx} +1 -1
- package/src/components/{DuffelAncillaries.tsx → DuffelAncillaries/DuffelAncillaries.tsx} +61 -63
- package/src/components/{DuffelAncillariesCustomElement.tsx → DuffelAncillaries/DuffelAncillariesCustomElement.tsx} +2 -2
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionCard.tsx +4 -4
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionController.tsx +2 -2
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModal.tsx +3 -3
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBody.tsx +3 -3
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalBodyPassenger.tsx +2 -2
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalFooter.tsx +2 -2
- package/src/components/{bags → DuffelAncillaries/bags}/BaggageSelectionModalHeader.tsx +1 -1
- package/src/components/{bags → DuffelAncillaries/bags}/IncludedBaggageBanner.tsx +1 -1
- package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionCard.tsx +4 -4
- package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModal.tsx +3 -3
- package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBody.tsx +3 -3
- package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBodyListItem.tsx +1 -1
- package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalFooter.tsx +4 -4
- package/src/components/{seats → DuffelAncillaries/seats}/Amenity.tsx +2 -2
- package/src/components/{seats → DuffelAncillaries/seats}/DeckSelect.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/Element.tsx +2 -2
- package/src/components/{seats → DuffelAncillaries/seats}/ExitElement.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/Legend.tsx +2 -2
- package/src/components/{seats → DuffelAncillaries/seats}/Row.tsx +2 -2
- package/src/components/{seats → DuffelAncillaries/seats}/RowSection.tsx +5 -2
- package/src/components/{seats → DuffelAncillaries/seats}/SeatElement.tsx +3 -3
- package/src/components/{seats → DuffelAncillaries/seats}/SeatInfo.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/SeatMap.tsx +2 -2
- package/src/components/{seats → DuffelAncillaries/seats}/SeatMapUnavailable.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionCard.tsx +5 -5
- package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModal.tsx +4 -4
- package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalBody.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalFooter.tsx +3 -3
- package/src/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalHeader.tsx +1 -1
- package/src/components/{seats → DuffelAncillaries/seats}/SeatUnavailable.tsx +2 -2
- package/src/components/DuffelPayments/DuffelPayments.tsx +218 -0
- package/src/components/DuffelPayments/DuffelPaymentsCustomElement.tsx +130 -0
- package/src/components/{Button.tsx → shared/Button.tsx} +4 -3
- package/src/components/{ErrorBoundary.tsx → shared/ErrorBoundary.tsx} +2 -2
- package/src/components/{Icon.tsx → shared/Icon.tsx} +2 -1
- package/src/components/{IconButton.tsx → shared/IconButton.tsx} +1 -1
- package/src/components/{Modal.tsx → shared/Modal.tsx} +5 -1
- package/src/components/{NonIdealState.tsx → shared/NonIdealState.tsx} +1 -1
- package/src/custom-elements.ts +13 -0
- package/src/examples/client-side/README.md +30 -0
- package/src/examples/client-side/index.html +1 -1
- package/src/examples/full-stack/README.md +34 -0
- package/src/examples/full-stack/index.html +1 -1
- package/src/examples/full-stack/server.mjs +1 -0
- package/src/examples/just-typescript/src/index.html +2 -2
- package/src/examples/just-typescript/src/index.ts +2 -1
- package/src/examples/payments-custom-element/README.md +17 -0
- package/src/examples/payments-custom-element/index.html +43 -0
- package/src/examples/payments-just-typescript/README.md +37 -0
- package/src/examples/payments-just-typescript/package.json +16 -0
- package/src/examples/payments-just-typescript/src/index.html +23 -0
- package/src/examples/payments-just-typescript/src/index.ts +18 -0
- package/src/examples/payments-just-typescript/yarn.lock +154 -0
- package/src/examples/react-app/src/index.tsx +11 -6
- package/src/index.ts +4 -5
- package/src/lib/captureErrorInSentry.ts +2 -20
- package/src/lib/fetchFromDuffelAPI.ts +36 -6
- package/src/lib/formatDate.ts +3 -4
- package/src/lib/getBaggageServiceDescription.ts +2 -4
- package/src/lib/getTotalAmountForServices.ts +1 -1
- package/src/lib/logging.ts +52 -32
- package/src/lib/retrieveOffer.ts +11 -6
- package/src/lib/retrieveSeatMaps.ts +13 -8
- package/src/stories/BaggageSelectionModalHeader.stories.tsx +1 -1
- package/src/stories/Button.stories.tsx +12 -3
- package/src/stories/DuffelAncillaries.stories.tsx +1 -1
- package/src/stories/DuffelPayments.stories.tsx +34 -0
- package/src/stories/Icon.stories.tsx +3 -2
- package/src/stories/IconButton.stories.tsx +1 -1
- package/src/styles/components/DuffelPayments.css +42 -0
- package/src/styles/components/Modal.css +2 -1
- package/src/styles/global.css +1 -0
- package/src/tests/components/DuffelAncillaries.test.tsx +1 -1
- package/src/tests/lib/createPriceFormatters.test.tsx +1 -1
- package/src/tests/lib/formatAvailableServices.test.tsx +1 -1
- package/src/tests/lib/formatSeatMaps.test.tsx +2 -2
- package/src/tests/lib/getCurrencyForServices.test.tsx +1 -1
- package/src/tests/lib/hasServiceOfSameMetadataTypeAlreadyBeenSelected.test.ts +1 -1
- package/src/tests/lib/logging.test.tsx +14 -14
- package/src/tests/lib/moneyStringFormatter.test.tsx +1 -1
- package/src/tests/lib/validateProps.test.tsx +1 -1
- package/src/types/DuffelAncillariesProps.ts +1 -1
- package/tsconfig.json +1 -1
- /package/react-dist/components/{Counter.d.ts → DuffelAncillaries/Counter.d.ts} +0 -0
- /package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalBodyListItem.d.ts +0 -0
- /package/react-dist/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalHeader.d.ts +0 -0
- /package/react-dist/components/{seats → DuffelAncillaries/seats}/DeckSelect.d.ts +0 -0
- /package/react-dist/components/{seats → DuffelAncillaries/seats}/EmptyElement.d.ts +0 -0
- /package/react-dist/components/{seats → DuffelAncillaries/seats}/ExitElement.d.ts +0 -0
- /package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatMapUnavailable.d.ts +0 -0
- /package/react-dist/components/{seats → DuffelAncillaries/seats}/SeatSelectionModalBody.d.ts +0 -0
- /package/react-dist/components/{AnimatedLoaderEllipsis.d.ts → shared/AnimatedLoaderEllipsis.d.ts} +0 -0
- /package/react-dist/components/{FetchOfferErrorState.d.ts → shared/FetchOfferErrorState.d.ts} +0 -0
- /package/react-dist/components/{Icon.d.ts → shared/Icon.d.ts} +0 -0
- /package/react-dist/components/{Modal.d.ts → shared/Modal.d.ts} +0 -0
- /package/react-dist/components/{Stamp.d.ts → shared/Stamp.d.ts} +0 -0
- /package/react-dist/components/{Tabs.d.ts → shared/Tabs.d.ts} +0 -0
- /package/src/components/{cancel_for_any_reason → DuffelAncillaries/cancel_for_any_reason}/CfarSelectionModalHeader.tsx +0 -0
- /package/src/components/{seats → DuffelAncillaries/seats}/EmptyElement.tsx +0 -0
- /package/src/components/{AnimatedLoaderEllipsis.tsx → shared/AnimatedLoaderEllipsis.tsx} +0 -0
- /package/src/components/{FetchOfferErrorState.tsx → shared/FetchOfferErrorState.tsx} +0 -0
- /package/src/components/{Stamp.tsx → shared/Stamp.tsx} +0 -0
- /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 "
|
|
4
|
-
import {
|
|
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 "
|
|
6
|
-
import { SeatMapCabinRowSectionElementSeat } from "
|
|
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 "
|
|
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 "
|
|
7
|
-
import { SeatMap as SeatMapType } from "
|
|
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,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 "
|
|
12
|
-
import { Offer } from "
|
|
13
|
-
import { SeatMap } from "
|
|
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 "
|
|
12
|
-
import { Offer } from "
|
|
13
|
-
import { SeatMap } from "
|
|
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,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 "
|
|
8
|
-
import { SeatMap } from "
|
|
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 "
|
|
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 "
|
|
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=
|
|
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
|
|
16
|
+
componentDidCatch(error: Error) {
|
|
17
17
|
// You can also log the error to an error reporting service
|
|
18
|
-
captureErrorInSentry(error
|
|
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
|
-
|
|
113
|
+
log(`The icon "${name}" is missing from ICON_MAP`);
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
115
116
|
return ICON_MAP[name];
|
|
@@ -16,7 +16,11 @@ export const Modal: React.FC<ModalProps> = ({ children, onClose, isOpen }) => {
|
|
|
16
16
|
}, [isOpen]);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<div
|
|
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
|
|
|
@@ -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`
|
|
@@ -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`
|