@lookiero/checkout 10.0.1 → 11.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 (74) hide show
  1. package/cypress/integration/checkout.spec.ts +0 -3
  2. package/dist/fake-dependencies/@lookiero/payments-front/index.d.ts +8 -7
  3. package/dist/fake-dependencies/@lookiero/payments-front/index.js +11 -3
  4. package/dist/index.d.ts +3 -3
  5. package/dist/src/ExpoRoot.js +17 -12
  6. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.d.ts +1 -1
  7. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.js +2 -0
  8. package/dist/src/infrastructure/projection/checkout/checkout.mock.d.ts +1 -0
  9. package/dist/src/infrastructure/projection/checkout/checkout.mock.js +3 -3
  10. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.d.ts +1 -1
  11. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.js +2 -1
  12. package/dist/src/infrastructure/tracking/tracking.d.ts +2 -2
  13. package/dist/src/infrastructure/tracking/useTrackCheckout.d.ts +10 -17
  14. package/dist/src/infrastructure/tracking/useTrackCheckout.js +27 -12
  15. package/dist/src/infrastructure/ui/Root.d.ts +6 -6
  16. package/dist/src/infrastructure/ui/Root.js +2 -3
  17. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.d.ts +26 -0
  18. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.js +127 -0
  19. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.d.ts +3 -2
  20. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.js +17 -26
  21. package/dist/src/infrastructure/ui/i18n/i18n.d.ts +1 -0
  22. package/dist/src/infrastructure/ui/i18n/i18n.js +1 -0
  23. package/dist/src/infrastructure/ui/routing/CheckoutMiddleware.js +1 -12
  24. package/dist/src/infrastructure/ui/routing/Routing.d.ts +5 -5
  25. package/dist/src/infrastructure/ui/routing/Routing.js +2 -10
  26. package/dist/src/infrastructure/ui/routing/routes.d.ts +0 -1
  27. package/dist/src/infrastructure/ui/routing/routes.js +0 -1
  28. package/dist/src/infrastructure/ui/views/App.js +5 -6
  29. package/dist/src/infrastructure/ui/views/checkout/Checkout.d.ts +7 -2
  30. package/dist/src/infrastructure/ui/views/checkout/Checkout.js +20 -9
  31. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.d.ts +3 -0
  32. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.js +3 -0
  33. package/dist/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.js +7 -7
  34. package/dist/src/projection/customer/customer.d.ts +2 -0
  35. package/dist/src/projection/order/order.d.ts +1 -1
  36. package/dist/src/projection/subscription/subscription.d.ts +1 -1
  37. package/dist/src/version.d.ts +1 -1
  38. package/dist/src/version.js +1 -1
  39. package/fake-dependencies/@lookiero/payments-front/index.tsx +36 -8
  40. package/index.ts +10 -3
  41. package/package.json +4 -4
  42. package/src/ExpoRoot.tsx +43 -36
  43. package/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.ts +4 -1
  44. package/src/infrastructure/projection/checkout/checkout.mock.ts +8 -3
  45. package/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.ts +3 -2
  46. package/src/infrastructure/tracking/tracking.ts +2 -2
  47. package/src/infrastructure/tracking/useTrackCheckout.test.tsx +51 -24
  48. package/src/infrastructure/tracking/useTrackCheckout.ts +66 -56
  49. package/src/infrastructure/ui/Root.tsx +9 -9
  50. package/src/infrastructure/ui/components/templates/header/itemHeader/ItemHeader.tsx +1 -0
  51. package/src/infrastructure/ui/hooks/useCheckoutFlow.test.tsx +302 -0
  52. package/src/infrastructure/ui/hooks/useCheckoutFlow.tsx +203 -0
  53. package/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.ts +18 -60
  54. package/src/infrastructure/ui/i18n/i18n.ts +1 -0
  55. package/src/infrastructure/ui/routing/CheckoutMiddleware.test.tsx +0 -11
  56. package/src/infrastructure/ui/routing/CheckoutMiddleware.tsx +1 -15
  57. package/src/infrastructure/ui/routing/Routing.tsx +14 -25
  58. package/src/infrastructure/ui/routing/routes.ts +0 -1
  59. package/src/infrastructure/ui/views/App.tsx +5 -13
  60. package/src/infrastructure/ui/views/checkout/Checkout.style.ts +3 -0
  61. package/src/infrastructure/ui/views/checkout/Checkout.test.tsx +51 -43
  62. package/src/infrastructure/ui/views/checkout/Checkout.tsx +51 -13
  63. package/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.tsx +8 -8
  64. package/src/projection/customer/customer.ts +2 -0
  65. package/src/projection/order/order.ts +1 -1
  66. package/src/projection/subscription/subscription.ts +1 -1
  67. package/dist/src/infrastructure/ui/hooks/useSubmitCheckout.d.ts +0 -27
  68. package/dist/src/infrastructure/ui/hooks/useSubmitCheckout.js +0 -97
  69. package/dist/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.d.ts +0 -12
  70. package/dist/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.js +0 -88
  71. package/src/infrastructure/ui/hooks/useSubmitCheckout.test.ts +0 -297
  72. package/src/infrastructure/ui/hooks/useSubmitCheckout.ts +0 -169
  73. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.test.tsx +0 -134
  74. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.tsx +0 -124
@@ -23,7 +23,7 @@ const DEFAULT_QUERY_OPTIONS: QueryOptions = {
23
23
  };
24
24
 
25
25
  interface UseViewPricingByCheckoutIdFunctionArgs {
26
- readonly checkoutId: string;
26
+ readonly checkoutId: string | undefined;
27
27
  readonly queryOptions?: QueryOptions;
28
28
  }
29
29
 
@@ -36,7 +36,7 @@ const useViewPricingByCheckoutId: UseViewPricingByCheckoutIdFunction = ({
36
36
  queryOptions = DEFAULT_QUERY_OPTIONS,
37
37
  }) =>
38
38
  useQuery({
39
- query: viewPricingByCheckoutId({ checkoutId }),
39
+ query: viewPricingByCheckoutId({ checkoutId: checkoutId as string }),
40
40
  contextId: MESSAGING_CONTEXT_ID,
41
41
  invalidation: shouldInvalidate,
42
42
  options: {
@@ -44,6 +44,7 @@ const useViewPricingByCheckoutId: UseViewPricingByCheckoutIdFunction = ({
44
44
  staleTime: Infinity,
45
45
  retry: false,
46
46
  refetchOnWindowFocus: false,
47
+ enabled: Boolean(checkoutId),
47
48
  },
48
49
  });
49
50
 
@@ -2,7 +2,7 @@ import { BaseTrackingEvent, TrackingEventCategory } from "@lookiero/sty-psp-trac
2
2
  import { CheckoutItemStatus } from "../../domain/checkoutItem/model/checkoutItem";
3
3
  import { Currency } from "../../domain/checkoutItem/model/currency";
4
4
  import { MediaPerspective } from "../../projection/checkoutItem/checkoutItem";
5
- import { Subscription } from "../../projection/subscription/subscription";
5
+ import { SubscriptionProjection } from "../../projection/subscription/subscription";
6
6
 
7
7
  const PROJECT = "checkout";
8
8
 
@@ -121,7 +121,7 @@ interface CheckoutTrackingEvent
121
121
  readonly actionField: {
122
122
  readonly items: number;
123
123
  readonly currencyCode: Currency;
124
- readonly subscription: Subscription;
124
+ readonly subscription: SubscriptionProjection;
125
125
  readonly coupon: string | null;
126
126
  readonly orderId: number;
127
127
  readonly value: number;
@@ -2,9 +2,15 @@ import { renderHook } from "@testing-library/react-native";
2
2
  import { Country } from "@lookiero/sty-psp-locale";
3
3
  import { Segment } from "@lookiero/sty-psp-segment";
4
4
  import { useEmitUserEvent } from "@lookiero/sty-psp-tracking";
5
+ import { CheckoutStatus } from "../../domain/checkout/model/checkout";
6
+ import { CheckoutItemStatus } from "../../domain/checkoutItem/model/checkoutItem";
5
7
  import { Currency } from "../../domain/checkoutItem/model/currency";
8
+ import { CheckoutProjection } from "../../projection/checkout/checkout";
9
+ import { OrderProjection } from "../../projection/order/order";
10
+ import { PricingProjection } from "../../projection/pricing/pricing";
11
+ import { SubscriptionProjection } from "../../projection/subscription/subscription";
12
+ import { checkout as checkoutMock } from "../projection/checkout/checkout.mock";
6
13
  import { CheckoutTrackingEvent, TrackingEventName, TrackingEventCategory } from "./tracking";
7
- import { TrackingPage } from "./tracking";
8
14
  import { useTrackCheckout as sut } from "./useTrackCheckout";
9
15
 
10
16
  const mockEmitUserEvent = jest.fn();
@@ -12,6 +18,34 @@ jest.mock("@lookiero/sty-psp-tracking");
12
18
 
13
19
  const country = Country.ES;
14
20
  const segment = Segment.WOMEN;
21
+ const userId = "80c38da1-7091-483d-a2ad-8f1642211e80";
22
+ const order: OrderProjection = {
23
+ coupon: null,
24
+ isFirstOrder: false,
25
+ orderNumber: 3691625,
26
+ };
27
+ const checkoutItemsReturned = 1;
28
+ const checkoutItemsKept = 2;
29
+ const checkoutItemsReplaced = 2;
30
+ const checkout: CheckoutProjection = checkoutMock({
31
+ id: "c513ebc8-df68-4f47-b899-8ec0ced84ca8",
32
+ status: CheckoutStatus.STARTED,
33
+ items: [
34
+ ...new Array(checkoutItemsReturned).fill(1).map(() => ({ status: CheckoutItemStatus.RETURNED })),
35
+ ...new Array(checkoutItemsKept).fill(1).map(() => ({ status: CheckoutItemStatus.KEPT })),
36
+ ...new Array(checkoutItemsReplaced).fill(1).map(() => ({
37
+ status: CheckoutItemStatus.REPLACED,
38
+ replacedFor: { id: "26184353-c4bd-467a-bfd5-c9f688956058" },
39
+ })),
40
+ ],
41
+ });
42
+ const pricing: PricingProjection = {
43
+ pendingToPay: {
44
+ amount: 16691,
45
+ currency: Currency.EUR,
46
+ },
47
+ } as PricingProjection;
48
+ const subscription: SubscriptionProjection = "b";
15
49
 
16
50
  const event: CheckoutTrackingEvent = {
17
51
  event: TrackingEventName.CHECKOUT,
@@ -19,19 +53,19 @@ const event: CheckoutTrackingEvent = {
19
53
  section: "checkout_checkout",
20
54
  store: country,
21
55
  segment,
22
- checkoutId: "c513ebc8-df68-4f47-b899-8ec0ced84ca8",
23
- userId: "80c38da1-7091-483d-a2ad-8f1642211e80",
24
- sizeChanges: 0,
25
- isFirstOrder: false,
56
+ checkoutId: checkout.id,
57
+ userId,
58
+ sizeChanges: checkoutItemsReplaced,
59
+ isFirstOrder: order.isFirstOrder,
26
60
  ecommerce: {
27
61
  checkout: {
28
62
  actionField: {
29
- items: 5,
30
- currencyCode: Currency.EUR,
31
- subscription: "b",
32
- coupon: null,
33
- orderId: 3691625,
34
- value: 166.91,
63
+ items: checkoutItemsKept + checkoutItemsReplaced,
64
+ currencyCode: pricing.pendingToPay.currency,
65
+ subscription,
66
+ coupon: order.coupon,
67
+ orderId: order.orderNumber,
68
+ value: pricing.pendingToPay.amount / 100,
35
69
  },
36
70
  },
37
71
  },
@@ -43,24 +77,17 @@ describe("useTrackCheckout custom hook", () => {
43
77
 
44
78
  const { result } = renderHook(() =>
45
79
  sut({
46
- page: TrackingPage.CHECKOUT,
80
+ order,
81
+ checkout,
82
+ pricing,
83
+ subscription,
84
+ userId,
47
85
  country,
48
86
  segment,
49
- checkoutId: "c513ebc8-df68-4f47-b899-8ec0ced84ca8",
50
87
  }),
51
88
  );
52
89
 
53
- result.current({
54
- totalCheckoutItemsKept: 5,
55
- pendingToPay: 166.91,
56
- coupon: null,
57
- currencyCode: Currency.EUR,
58
- isFirstOrder: false,
59
- orderNumber: 3691625,
60
- totalReplacedFor: 0,
61
- subscription: "b",
62
- userId: "80c38da1-7091-483d-a2ad-8f1642211e80",
63
- });
90
+ result.current();
64
91
 
65
92
  expect(mockEmitUserEvent).toHaveBeenCalledWith(event);
66
93
  });
@@ -1,84 +1,94 @@
1
1
  import { useCallback } from "react";
2
+ import invariant from "tiny-invariant";
2
3
  import { Country } from "@lookiero/sty-psp-locale";
3
4
  import { Segment } from "@lookiero/sty-psp-segment";
4
5
  import { useEmitUserEvent } from "@lookiero/sty-psp-tracking";
6
+ import { CheckoutItemStatus } from "../../domain/checkoutItem/model/checkoutItem";
5
7
  import { Currency } from "../../domain/checkoutItem/model/currency";
6
- import { Subscription } from "../../projection/subscription/subscription";
8
+ import { CheckoutProjection } from "../../projection/checkout/checkout";
9
+ import { OrderProjection } from "../../projection/order/order";
10
+ import { PricingProjection } from "../../projection/pricing/pricing";
11
+ import { SubscriptionProjection } from "../../projection/subscription/subscription";
7
12
  import { PROJECT, TrackingPage } from "./tracking";
8
13
  import { CheckoutTrackingEvent, TrackingEventName, TrackingEventCategory } from "./tracking";
9
14
 
10
- interface TrackCheckoutFunctionArgs {
11
- readonly userId: string;
12
- readonly totalReplacedFor: number;
13
- readonly isFirstOrder: boolean;
14
- readonly totalCheckoutItemsKept: number;
15
- readonly currencyCode: Currency;
16
- readonly subscription: Subscription;
17
- readonly coupon: string | null;
18
- readonly orderNumber: number;
19
- readonly pendingToPay: number;
20
- }
21
15
  interface TrackCheckoutFunction {
22
- (args: TrackCheckoutFunctionArgs): void;
16
+ (): void;
23
17
  }
24
18
 
25
19
  interface UseTrackCheckoutFunctionArgs {
26
- readonly page: TrackingPage;
20
+ readonly order: OrderProjection;
21
+ readonly checkout: CheckoutProjection | undefined;
22
+ readonly pricing: PricingProjection | undefined;
23
+ readonly subscription: SubscriptionProjection;
24
+ readonly userId: string;
27
25
  readonly country: Country;
28
- readonly checkoutId: string | undefined;
29
26
  readonly segment: Segment;
30
27
  }
28
+
31
29
  interface UseTrackCheckoutFunction {
32
30
  (args: UseTrackCheckoutFunctionArgs): TrackCheckoutFunction;
33
31
  }
34
32
 
35
- const useTrackCheckout: UseTrackCheckoutFunction = ({ page, country, checkoutId, segment }) => {
33
+ const useTrackCheckout: UseTrackCheckoutFunction = ({
34
+ order,
35
+ checkout,
36
+ pricing,
37
+ subscription,
38
+ segment,
39
+ country,
40
+ userId,
41
+ }) => {
36
42
  const emitUserEvent = useEmitUserEvent<CheckoutTrackingEvent>();
43
+ const { coupon, isFirstOrder, orderNumber } = order;
37
44
 
38
- const trackCheckout: TrackCheckoutFunction = useCallback(
39
- ({
40
- coupon,
41
- currencyCode,
42
- isFirstOrder,
43
- orderNumber,
44
- pendingToPay,
45
- subscription,
46
- totalCheckoutItemsKept,
47
- totalReplacedFor,
48
- userId,
49
- }) => {
50
- if (!checkoutId) {
51
- return;
52
- }
45
+ const trackCheckout: TrackCheckoutFunction = useCallback(() => {
46
+ invariant(checkout, "Checkout must be defined to track checkout");
47
+
48
+ const checkoutItemsKept = checkout.items.filter(
49
+ (checkoutItem) =>
50
+ checkoutItem.status === CheckoutItemStatus.KEPT || checkoutItem.status === CheckoutItemStatus.REPLACED,
51
+ );
52
+ const totalReplacedFor = checkoutItemsKept?.filter(({ replacedFor }) => Boolean(replacedFor)).length || 0;
53
53
 
54
- const checkoutTrackingEvent: CheckoutTrackingEvent = {
55
- event: TrackingEventName.CHECKOUT,
56
- eventCategory: TrackingEventCategory.ECOMMERCE,
57
- section: `${PROJECT}_${page}`,
58
- store: country,
59
- segment,
60
- checkoutId,
61
- userId,
62
- sizeChanges: totalReplacedFor,
63
- isFirstOrder,
64
- ecommerce: {
65
- checkout: {
66
- actionField: {
67
- items: totalCheckoutItemsKept,
68
- currencyCode,
69
- subscription,
70
- coupon,
71
- orderId: orderNumber,
72
- value: pendingToPay,
73
- },
54
+ const checkoutTrackingEvent: CheckoutTrackingEvent = {
55
+ event: TrackingEventName.CHECKOUT,
56
+ eventCategory: TrackingEventCategory.ECOMMERCE,
57
+ section: `${PROJECT}_${TrackingPage.CHECKOUT}`,
58
+ store: country,
59
+ segment,
60
+ checkoutId: checkout.id,
61
+ userId,
62
+ sizeChanges: totalReplacedFor,
63
+ isFirstOrder,
64
+ ecommerce: {
65
+ checkout: {
66
+ actionField: {
67
+ items: checkoutItemsKept.length || 0,
68
+ currencyCode: pricing?.pendingToPay.currency as Currency,
69
+ subscription,
70
+ coupon,
71
+ orderId: orderNumber,
72
+ value: (pricing?.pendingToPay.amount as number) / 100,
74
73
  },
75
74
  },
76
- };
75
+ },
76
+ };
77
77
 
78
- emitUserEvent(checkoutTrackingEvent);
79
- },
80
- [checkoutId, country, emitUserEvent, page, segment],
81
- );
78
+ emitUserEvent(checkoutTrackingEvent);
79
+ }, [
80
+ checkout,
81
+ country,
82
+ coupon,
83
+ emitUserEvent,
84
+ isFirstOrder,
85
+ orderNumber,
86
+ pricing?.pendingToPay.amount,
87
+ pricing?.pendingToPay.currency,
88
+ segment,
89
+ subscription,
90
+ userId,
91
+ ]);
82
92
 
83
93
  return trackCheckout;
84
94
  };
@@ -10,8 +10,8 @@ import { SentryEnvironment, SentryLoggerFunctionArgs, sentryLogger, sentryLogger
10
10
  import { Layout } from "@lookiero/sty-psp-ui";
11
11
  import { Tradename } from "@lookiero/sty-sp-tradename";
12
12
  import { Customer } from "../../projection/customer/customer";
13
- import { Order } from "../../projection/order/order";
14
- import { Subscription } from "../../projection/subscription/subscription";
13
+ import { OrderProjection } from "../../projection/order/order";
14
+ import { SubscriptionProjection } from "../../projection/subscription/subscription";
15
15
  import { KameleoonEnvironment } from "../ab-testing/kameleoonEnvironment";
16
16
  import { QueryBusProvider } from "./hooks/useQueryBus";
17
17
  import { Routing } from "./routing/Routing";
@@ -32,14 +32,14 @@ interface RootFunction {
32
32
 
33
33
  interface RootProps {
34
34
  readonly basePath: string;
35
- readonly locale?: Locale;
35
+ readonly locale: Locale;
36
36
  readonly customer: Customer;
37
- readonly order: Order | undefined;
38
- readonly subscription: Subscription | undefined;
37
+ readonly order: OrderProjection;
38
+ readonly subscription: SubscriptionProjection;
39
39
  readonly layout: Layout;
40
40
  readonly tradename: Tradename;
41
41
  readonly onNotAccessible: () => void;
42
- readonly onCheckoutSubmitted?: () => void;
42
+ readonly onCheckoutFlowSuccess: () => void;
43
43
  readonly useRedirect: () => Record<string, string>;
44
44
  readonly useRoutes?: typeof reactRouterUseRoutes;
45
45
  }
@@ -59,14 +59,14 @@ const root: RootFunction = ({
59
59
  // eslint-disable-next-line react/display-name, react/prop-types
60
60
  const Root = ({
61
61
  basePath,
62
- locale = Locale.en_GB,
62
+ locale,
63
63
  customer,
64
64
  order,
65
65
  subscription,
66
66
  layout,
67
67
  tradename,
68
68
  onNotAccessible,
69
- onCheckoutSubmitted,
69
+ onCheckoutFlowSuccess,
70
70
  useRedirect,
71
71
  useRoutes = reactRouterUseRoutes,
72
72
  }: RootProps) => {
@@ -88,7 +88,7 @@ const root: RootFunction = ({
88
88
  tradename={tradename}
89
89
  useRedirect={useRedirect}
90
90
  useRoutes={useRoutes}
91
- onCheckoutSubmitted={onCheckoutSubmitted}
91
+ onCheckoutFlowSuccess={onCheckoutFlowSuccess}
92
92
  onI18nError={development ? undefined : handleOnI18nError}
93
93
  onNotAccessible={onNotAccessible}
94
94
  />
@@ -33,6 +33,7 @@ interface ItemHeaderProps {
33
33
  readonly onNext?: OnNextItemFunction;
34
34
  readonly onPrevious?: OnPreviousItemFunction;
35
35
  }
36
+
36
37
  const ItemHeader: FC<ItemHeaderProps> = ({ items, item, onNext, onPrevious }) => {
37
38
  const title = useI18nMessage({ domain: DOMAIN, id: I18nMessages.HEADER_CHECKOUT_TITLE });
38
39
  const itemIndex = items.indexOf(item);
@@ -0,0 +1,302 @@
1
+ import { act, render, renderHook, waitFor } from "@testing-library/react-native";
2
+ import React from "react";
3
+ import { CommandStatus, QueryStatus } from "@lookiero/messaging-react";
4
+ import { Country } from "@lookiero/sty-psp-locale";
5
+ import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
6
+ import { Segment } from "@lookiero/sty-psp-segment";
7
+ import { CheckoutItemStatus } from "../../../domain/checkoutItem/model/checkoutItem";
8
+ import { CheckoutBookingProjection } from "../../../projection/checkoutBooking/checkoutBooking";
9
+ import { OrderProjection } from "../../../projection/order/order";
10
+ import { SubscriptionProjection } from "../../../projection/subscription/subscription";
11
+ import { useSubmitCheckout } from "../../domain/checkout/react/useSubmitCheckout";
12
+ import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
13
+ import { checkout } from "../../projection/checkout/checkout.mock";
14
+ import { useViewIsSizeChangeEnabledByCheckoutId } from "../../projection/checkout/react/useViewIsSizeChangeEnabledByCheckoutId";
15
+ import { paymentFlowPayload as mockPaymentFlowPayload } from "../../projection/payment/paymentFlowPayload.mock";
16
+ import { useViewPaymentFlowPayloadByCheckoutId } from "../../projection/payment/react/useViewPaymentFlowPayloadByCheckoutId";
17
+ import { pricing } from "../../projection/pricing/pricing.mock";
18
+ import { useViewPricingByCheckoutId } from "../../projection/pricing/react/useViewPricingByCheckoutId";
19
+ import { I18nMessages } from "../i18n/i18n";
20
+ import { Routes } from "../routing/routes";
21
+ import { useCheckoutFlow as sut } from "./useCheckoutFlow";
22
+ import { usePaymentInstrumentEvents } from "./usePaymentInstrumentEvents";
23
+ import { useQueryBus } from "./useQueryBus";
24
+
25
+ const getAuthToken = () => Promise.resolve("token");
26
+ const mockCheckout = checkout({
27
+ items: [
28
+ { status: CheckoutItemStatus.KEPT },
29
+ { status: CheckoutItemStatus.KEPT },
30
+ { status: CheckoutItemStatus.KEPT },
31
+ { status: CheckoutItemStatus.KEPT },
32
+ { status: CheckoutItemStatus.RETURNED },
33
+ ],
34
+ });
35
+ const order: OrderProjection = {
36
+ orderNumber: 12345,
37
+ isFirstOrder: false,
38
+ coupon: null,
39
+ };
40
+ const mockPricing = pricing();
41
+ const subscription: SubscriptionProjection = "o";
42
+ const customerId = "a8fff6d7-708c-41a7-b42a-58c5706d33df";
43
+ const basePath = "/checkout";
44
+ const country = Country.ES;
45
+ const email = "email@example.com";
46
+ const name = "Adèle Léonce Émilie";
47
+ const segment = Segment.WOMEN;
48
+ jest.mock("./useStaticInfo", () => ({
49
+ useStaticInfo: () => ({ customer: { customerId, country, segment, name, email }, basePath }),
50
+ }));
51
+
52
+ // const errorChargeStatuses = Object.values(ChargeStatus).filter((status) => status !== ChargeStatus.EXECUTED);
53
+ const mockStartLegacyBoxCheckout = jest.fn();
54
+ jest.mock("@lookiero/payments-front", () => {
55
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
56
+ const { useImperativeHandle, forwardRef } = require("react");
57
+
58
+ return {
59
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
60
+ // @ts-ignore
61
+ // eslint-disable-next-line @typescript-eslint/naming-convention, react/display-name
62
+ PaymentFlow: forwardRef((params, ref) => {
63
+ useImperativeHandle(ref, () => ({
64
+ startLegacyBoxCheckout: mockStartLegacyBoxCheckout,
65
+ }));
66
+
67
+ return null;
68
+ }),
69
+ Section: {
70
+ ["BOX_CHECKOUT"]: "box-checkout",
71
+ },
72
+ };
73
+ });
74
+
75
+ jest.mock("@lookiero/sty-psp-logging", () => ({
76
+ useLogger: () => jest.fn(),
77
+ }));
78
+
79
+ jest.mock("@lookiero/sty-psp-notifications");
80
+ jest.mock("./useQueryBus");
81
+ jest.mock("../../domain/checkout/react/useSubmitCheckout");
82
+ jest.mock("../../domain/checkoutBooking/react/useBlockCheckoutBooking");
83
+ jest.mock("../../domain/checkout/react/useSubmitCheckout");
84
+ jest.mock("../../projection/payment/react/useViewPaymentFlowPayloadByCheckoutId");
85
+ jest.mock("../../projection/checkout/react/useViewIsSizeChangeEnabledByCheckoutId");
86
+ jest.mock("../../projection/pricing/react/useViewPricingByCheckoutId");
87
+ jest.mock("./usePaymentInstrumentEvents");
88
+
89
+ beforeEach(() => {
90
+ mockStartLegacyBoxCheckout.mockClear();
91
+ });
92
+
93
+ describe("useCheckoutFlow custom hook", () => {
94
+ test("successfully executes 'checkoutFlow'", async () => {
95
+ const mockBlockCheckoutBooking = jest.fn();
96
+ const mockSubmitCheckout = jest.fn();
97
+ const mockCreateToastNotification = jest.fn();
98
+ const mockOnSuccess = jest.fn();
99
+
100
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
101
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
102
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
103
+ (useViewPaymentFlowPayloadByCheckoutId as jest.Mock).mockReturnValue([mockPaymentFlowPayload, QueryStatus.SUCCESS]);
104
+ (useViewIsSizeChangeEnabledByCheckoutId as jest.Mock).mockReturnValue([true, QueryStatus.SUCCESS]);
105
+ (useViewPricingByCheckoutId as jest.Mock).mockReturnValue([mockPricing, QueryStatus.SUCCESS]);
106
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, "success"]);
107
+ (usePaymentInstrumentEvents as jest.Mock).mockImplementation(({ onSuccess }) => {
108
+ setTimeout(() => onSuccess(), 1000);
109
+ });
110
+
111
+ const { result } = renderHook(() =>
112
+ sut({ checkout: mockCheckout, order, subscription, getAuthToken, onSuccess: mockOnSuccess }),
113
+ );
114
+
115
+ let checkoutFlow: () => void, status, paymentFlowComponent;
116
+
117
+ await waitFor(() => {
118
+ [checkoutFlow, , paymentFlowComponent] = result.current;
119
+
120
+ expect(paymentFlowComponent).not.toBeNull();
121
+ });
122
+
123
+ render(<>{paymentFlowComponent}</>);
124
+
125
+ await act(async () => {
126
+ await checkoutFlow();
127
+ });
128
+
129
+ await waitFor(() => {
130
+ [, status] = result.current;
131
+
132
+ expect(status).toBe("success");
133
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
134
+ expect(mockStartLegacyBoxCheckout).toHaveBeenCalledWith(
135
+ expect.objectContaining({
136
+ ...mockPaymentFlowPayload,
137
+ userInformation: { email, name },
138
+ returnUrl: `${basePath}/${Routes.CHECKOUT}`,
139
+ }),
140
+ );
141
+ expect(mockSubmitCheckout).toHaveBeenCalled();
142
+ expect(mockCreateToastNotification).toHaveBeenCalledWith({
143
+ bodyI18nKey: I18nMessages.CHECKOUT_TOAST_PAYMENT_SUCCESS,
144
+ level: NotificationLevel.SUCCESS,
145
+ });
146
+ expect(mockOnSuccess).toHaveBeenCalled();
147
+ });
148
+ });
149
+
150
+ test("does not call blockCheckoutBooking if sizeChange is not enabled", async () => {
151
+ const mockBlockCheckoutBooking = jest.fn();
152
+ const mockSubmitCheckout = jest.fn();
153
+ const mockCreateToastNotification = jest.fn();
154
+ const mockOnSuccess = jest.fn();
155
+
156
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
157
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
158
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
159
+ (useViewPaymentFlowPayloadByCheckoutId as jest.Mock).mockReturnValue([mockPaymentFlowPayload, QueryStatus.SUCCESS]);
160
+ (useViewIsSizeChangeEnabledByCheckoutId as jest.Mock).mockReturnValue([false, QueryStatus.SUCCESS]);
161
+ (useViewPricingByCheckoutId as jest.Mock).mockReturnValue([mockPricing, QueryStatus.SUCCESS]);
162
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, "success"]);
163
+ (usePaymentInstrumentEvents as jest.Mock).mockImplementation(({ onSuccess }) => {
164
+ setTimeout(() => onSuccess(), 1000);
165
+ });
166
+
167
+ const { result } = renderHook(() =>
168
+ sut({ checkout: mockCheckout, order, subscription, getAuthToken, onSuccess: mockOnSuccess }),
169
+ );
170
+
171
+ let checkoutFlow: () => void, status, paymentFlowComponent;
172
+
173
+ await waitFor(() => {
174
+ [checkoutFlow, , paymentFlowComponent] = result.current;
175
+
176
+ expect(paymentFlowComponent).not.toBeNull();
177
+ });
178
+
179
+ render(<>{paymentFlowComponent}</>);
180
+
181
+ await act(async () => {
182
+ await checkoutFlow();
183
+ });
184
+
185
+ await waitFor(() => {
186
+ [, status] = result.current;
187
+
188
+ expect(status).toBe("success");
189
+ expect(mockBlockCheckoutBooking).not.toHaveBeenCalled();
190
+ expect(mockStartLegacyBoxCheckout).toHaveBeenCalledWith(
191
+ expect.objectContaining({
192
+ ...mockPaymentFlowPayload,
193
+ userInformation: { email, name },
194
+ returnUrl: `${basePath}/${Routes.CHECKOUT}`,
195
+ }),
196
+ );
197
+ expect(mockSubmitCheckout).toHaveBeenCalled();
198
+ expect(mockCreateToastNotification).toHaveBeenCalledWith({
199
+ bodyI18nKey: I18nMessages.CHECKOUT_TOAST_PAYMENT_SUCCESS,
200
+ level: NotificationLevel.SUCCESS,
201
+ });
202
+ expect(mockOnSuccess).toHaveBeenCalled();
203
+ });
204
+ });
205
+
206
+ test("breaks execution and returns error as the status when blockCheckoutBooking fails", async () => {
207
+ const mockBlockCheckoutBooking = jest.fn().mockRejectedValue("error");
208
+ const mockSubmitCheckout = jest.fn();
209
+ const mockCreateToastNotification = jest.fn();
210
+ const mockOnSuccess = jest.fn();
211
+
212
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
213
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.ERROR]);
214
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
215
+ (useViewPaymentFlowPayloadByCheckoutId as jest.Mock).mockReturnValue([mockPaymentFlowPayload, QueryStatus.SUCCESS]);
216
+ (useViewIsSizeChangeEnabledByCheckoutId as jest.Mock).mockReturnValue([true, QueryStatus.SUCCESS]);
217
+ (useViewPricingByCheckoutId as jest.Mock).mockReturnValue([mockPricing, QueryStatus.SUCCESS]);
218
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, "success"]);
219
+ (usePaymentInstrumentEvents as jest.Mock).mockImplementation(({ onSuccess }) => {
220
+ setTimeout(() => onSuccess(), 1000);
221
+ });
222
+
223
+ const { result } = renderHook(() =>
224
+ sut({ checkout: mockCheckout, order, subscription, getAuthToken, onSuccess: mockOnSuccess }),
225
+ );
226
+
227
+ let checkoutFlow: () => void, status, paymentFlowComponent;
228
+
229
+ await waitFor(() => {
230
+ [checkoutFlow, , paymentFlowComponent] = result.current;
231
+
232
+ expect(paymentFlowComponent).not.toBeNull();
233
+ });
234
+
235
+ render(<>{paymentFlowComponent}</>);
236
+
237
+ await act(async () => {
238
+ await checkoutFlow();
239
+ });
240
+
241
+ await waitFor(() => {
242
+ [, status] = result.current;
243
+
244
+ expect(status).toBe("error");
245
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
246
+ expect(mockStartLegacyBoxCheckout).not.toHaveBeenCalled();
247
+ expect(mockSubmitCheckout).not.toHaveBeenCalled();
248
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
249
+ expect(mockOnSuccess).not.toHaveBeenCalled();
250
+ });
251
+ });
252
+
253
+ test("shows a notification and returns error as the status when payment fails", async () => {
254
+ const mockBlockCheckoutBooking = jest.fn();
255
+ const mockSubmitCheckout = jest.fn();
256
+ const mockCreateToastNotification = jest.fn();
257
+ const mockOnSuccess = jest.fn();
258
+
259
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
260
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
261
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
262
+ (useViewPaymentFlowPayloadByCheckoutId as jest.Mock).mockReturnValue([mockPaymentFlowPayload, QueryStatus.SUCCESS]);
263
+ (useViewIsSizeChangeEnabledByCheckoutId as jest.Mock).mockReturnValue([true, QueryStatus.SUCCESS]);
264
+ (useViewPricingByCheckoutId as jest.Mock).mockReturnValue([mockPricing, QueryStatus.SUCCESS]);
265
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, "success"]);
266
+ (usePaymentInstrumentEvents as jest.Mock).mockImplementation(({ onError }) => {
267
+ setTimeout(() => onError({ metadata: null }), 1000);
268
+ });
269
+
270
+ const { result } = renderHook(() =>
271
+ sut({ checkout: mockCheckout, order, subscription, getAuthToken, onSuccess: mockOnSuccess }),
272
+ );
273
+
274
+ let checkoutFlow: () => void, status, paymentFlowComponent;
275
+
276
+ await waitFor(() => {
277
+ [checkoutFlow, , paymentFlowComponent] = result.current;
278
+
279
+ expect(paymentFlowComponent).not.toBeNull();
280
+ });
281
+
282
+ render(<>{paymentFlowComponent}</>);
283
+
284
+ await act(async () => {
285
+ await checkoutFlow();
286
+ });
287
+
288
+ await waitFor(() => {
289
+ [, status] = result.current;
290
+
291
+ expect(status).toBe("error");
292
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
293
+ expect(mockStartLegacyBoxCheckout).toHaveBeenCalled();
294
+ expect(mockSubmitCheckout).not.toHaveBeenCalled();
295
+ expect(mockCreateToastNotification).toHaveBeenCalledWith({
296
+ bodyI18nKey: I18nMessages.CHECKOUT_TOAST_PAYMENT_ERROR,
297
+ level: NotificationLevel.ERROR,
298
+ });
299
+ expect(mockOnSuccess).not.toHaveBeenCalled();
300
+ });
301
+ });
302
+ });