@lookiero/checkout 9.8.2 → 9.8.3

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.
@@ -24,7 +24,7 @@ const order = {
24
24
  coupon: "MYLOOKIERO",
25
25
  };
26
26
  const customer = {
27
- customerId: "74cb4f76-3f14-4983-81a6-2ee2a9a275d4",
27
+ customerId: "d3c2354b-9811-46d4-b2ba-5fba012ed94d",
28
28
  country: Country.ES,
29
29
  segment: Segment.WOMEN,
30
30
  };
@@ -79,7 +79,7 @@ const kameleoonConfig = {
79
79
  siteCode: "aplm4v3ckn",
80
80
  experiments: {},
81
81
  };
82
- const { Component: Messaging } = process.env.EXPO_PUBLIC_APP_VARIANT === "test"
82
+ const { Component: Messaging, queryBus } = process.env.EXPO_PUBLIC_APP_VARIANT === "test"
83
83
  ? checkoutMockBootstrap()
84
84
  : checkoutBootstrap({ apiUrl: () => apiUrl, getAuthToken });
85
85
  const I18n = i18n({
@@ -109,7 +109,7 @@ const ExpoRoot = () => {
109
109
  isAccessible === false && React.createElement(Text, { heading: true }, "Checkout is not accessible!"),
110
110
  React.createElement(Router, null,
111
111
  React.createElement(Routes, null,
112
- React.createElement(Route, { path: "/checkout/*", element: React.createElement(Root, { basePath: "/checkout", customer: customer, layout: DummyLayout, locale: locale, order: order, subscription: subscription, useRedirect: useRedirect, onNotAccessible: onNotAccessible }) }),
112
+ React.createElement(Route, { path: "/checkout/*", element: React.createElement(Root, { basePath: "/checkout", customer: customer, layout: DummyLayout, locale: locale, order: order, queryBus: queryBus, subscription: subscription, useRedirect: useRedirect, onNotAccessible: onNotAccessible }) }),
113
113
  React.createElement(Route, { element: React.createElement(Navigate, { to: "/checkout", replace: true }), path: "*" }))))))) : null;
114
114
  };
115
115
  export { ExpoRoot };
@@ -1,6 +1,7 @@
1
1
  import { ComponentType } from "react";
2
2
  import { useRoutes as reactRouterUseRoutes } from "react-router-native";
3
3
  import { I18n } from "@lookiero/i18n-react";
4
+ import { QueryBus } from "@lookiero/messaging";
4
5
  import { MessagingRoot } from "@lookiero/messaging-react/bootstrap";
5
6
  import { Locale } from "@lookiero/sty-psp-locale";
6
7
  import { SentryEnvironment, SentryLoggerFunctionArgs } from "@lookiero/sty-psp-logging";
@@ -27,6 +28,7 @@ interface RootProps {
27
28
  readonly order: Order | undefined;
28
29
  readonly subscription: Subscription | undefined;
29
30
  readonly layout: Layout;
31
+ readonly queryBus: QueryBus;
30
32
  readonly onNotAccessible: () => void;
31
33
  readonly onCheckoutSubmitted?: () => void;
32
34
  readonly useRedirect: () => Record<string, string>;
@@ -4,15 +4,17 @@ import { Platform } from "react-native";
4
4
  import { useRoutes as reactRouterUseRoutes } from "react-router-native";
5
5
  import { Locale } from "@lookiero/sty-psp-locale";
6
6
  import { sentryLogger, sentryLoggerHOC } from "@lookiero/sty-psp-logging";
7
+ import { QueryBusProvider } from "./hooks/useQueryBus";
7
8
  import { Routing } from "./routing/Routing";
8
9
  const root = ({ Messaging, I18n, getAuthToken, development, sentry, kameleoon: kameleoonConfig }) => {
9
10
  const logger = sentryLogger(sentry);
10
11
  const kameleoon = kameleoonConfig();
11
12
  // eslint-disable-next-line react/display-name, react/prop-types
12
- const Root = ({ basePath, locale = Locale.en_GB, customer, order, subscription, layout, onNotAccessible, onCheckoutSubmitted, useRedirect, useRoutes = reactRouterUseRoutes, }) => {
13
+ const Root = ({ basePath, locale = Locale.en_GB, customer, order, subscription, layout, queryBus, onNotAccessible, onCheckoutSubmitted, useRedirect, useRoutes = reactRouterUseRoutes, }) => {
13
14
  const handleOnI18nError = useCallback((error) => logger.captureException(error), []);
14
15
  return (React.createElement(Messaging, { includeReactQueryDevTools: Platform.OS === "web" },
15
- React.createElement(Routing, { I18n: I18n, basePath: basePath, customer: customer, getAuthToken: getAuthToken, kameleoon: kameleoon, layout: layout, locale: locale, order: order, subscription: subscription, useRedirect: useRedirect, useRoutes: useRoutes, onCheckoutSubmitted: onCheckoutSubmitted, onI18nError: development ? undefined : handleOnI18nError, onNotAccessible: onNotAccessible })));
16
+ React.createElement(QueryBusProvider, { queryBus: queryBus },
17
+ React.createElement(Routing, { I18n: I18n, basePath: basePath, customer: customer, getAuthToken: getAuthToken, kameleoon: kameleoon, layout: layout, locale: locale, order: order, subscription: subscription, useRedirect: useRedirect, useRoutes: useRoutes, onCheckoutSubmitted: onCheckoutSubmitted, onI18nError: development ? undefined : handleOnI18nError, onNotAccessible: onNotAccessible }))));
16
18
  };
17
19
  const hoc = sentryLoggerHOC({ logger });
18
20
  /**
@@ -0,0 +1,9 @@
1
+ import { FC, ReactNode } from "react";
2
+ import { QueryBus } from "@lookiero/messaging";
3
+ interface QueryBusProviderProps {
4
+ readonly children: ReactNode;
5
+ readonly queryBus: QueryBus;
6
+ }
7
+ declare const QueryBusProvider: FC<QueryBusProviderProps>;
8
+ declare const useQueryBus: () => QueryBus;
9
+ export { useQueryBus, QueryBusProvider };
@@ -0,0 +1,10 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import invariant from "tiny-invariant";
3
+ const QueryBusContext = createContext(null);
4
+ const QueryBusProvider = ({ children, queryBus }) => (React.createElement(QueryBusContext.Provider, { value: queryBus }, children));
5
+ const useQueryBus = () => {
6
+ const queryBus = useContext(QueryBusContext);
7
+ invariant(queryBus, "Your are trying to use the useQueryBus hook without wrapping your app with the <QueryBusProvider>.");
8
+ return queryBus;
9
+ };
10
+ export { useQueryBus, QueryBusProvider };
@@ -1,10 +1,12 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
2
  import { CommandStatus } from "@lookiero/messaging-react";
3
3
  import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
4
+ import { viewCheckoutBookingById, } from "../../../projection/checkoutBooking/viewCheckoutBookingById";
4
5
  import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
5
6
  import { useSubmitCheckout as useSubmitCheckoutCommand } from "../../domain/checkout/react/useSubmitCheckout";
6
7
  import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
7
8
  import { I18nMessages } from "../i18n/i18n";
9
+ import { useQueryBus } from "./useQueryBus";
8
10
  var ChargeStatus;
9
11
  (function (ChargeStatus) {
10
12
  ChargeStatus["EXECUTED"] = "EXECUTED";
@@ -17,9 +19,11 @@ var ChargeStatus;
17
19
  ChargeStatus["UNKNOWN"] = "UNKNOWN";
18
20
  })(ChargeStatus || (ChargeStatus = {}));
19
21
  const useSubmitCheckout = ({ checkoutId, checkoutBookingId, paymentFlowRef, onError, onSuccess, logger, }) => {
22
+ const queryBus = useQueryBus();
20
23
  const [submitCheckoutCommand, submitCheckoutCommandStatus] = useSubmitCheckoutCommand({ checkoutId, logger });
21
24
  const [blockCheckoutBooking, blockCheckoutBookingStatus] = useBlockCheckoutBooking({ checkoutBookingId, logger });
22
25
  const [createNotification] = useCreateToastNotification({ contextId: MESSAGING_CONTEXT_ID, logger });
26
+ const [checkoutBookingExpired, setCheckoutBookingExpired] = useState(false);
23
27
  const [startLegacyBoxCheckoutStatus, setStartLegacyBoxCheckoutStatus] = useState("idle");
24
28
  const submitCheckout = useCallback(async ({ paymentFlowPayload, sizeChangeEnabled }) => {
25
29
  try {
@@ -28,6 +32,11 @@ const useSubmitCheckout = ({ checkoutId, checkoutBookingId, paymentFlowRef, onEr
28
32
  catch (error) {
29
33
  return;
30
34
  }
35
+ const checkoutBooking = await queryBus(viewCheckoutBookingById({ checkoutBookingId }));
36
+ if (checkoutBooking?.isExpired) {
37
+ setCheckoutBookingExpired(true);
38
+ return;
39
+ }
31
40
  paymentFlowRef.current?.startLegacyBoxCheckout(
32
41
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
33
42
  // @ts-ignore
@@ -48,7 +57,15 @@ const useSubmitCheckout = ({ checkoutId, checkoutBookingId, paymentFlowRef, onEr
48
57
  }
49
58
  }
50
59
  });
51
- }, [paymentFlowRef, blockCheckoutBooking, submitCheckoutCommand, onSuccess, createNotification]);
60
+ }, [
61
+ queryBus,
62
+ checkoutBookingId,
63
+ paymentFlowRef,
64
+ blockCheckoutBooking,
65
+ submitCheckoutCommand,
66
+ onSuccess,
67
+ createNotification,
68
+ ]);
52
69
  const status = useMemo(() => {
53
70
  if (blockCheckoutBookingStatus === CommandStatus.LOADING ||
54
71
  startLegacyBoxCheckoutStatus === "loading" ||
@@ -62,12 +79,19 @@ const useSubmitCheckout = ({ checkoutId, checkoutBookingId, paymentFlowRef, onEr
62
79
  }
63
80
  if (blockCheckoutBookingStatus === CommandStatus.ERROR ||
64
81
  startLegacyBoxCheckoutStatus === "error" ||
65
- submitCheckoutCommandStatus === CommandStatus.ERROR) {
82
+ submitCheckoutCommandStatus === CommandStatus.ERROR ||
83
+ checkoutBookingExpired) {
66
84
  onError();
67
85
  return "error";
68
86
  }
69
87
  return "idle";
70
- }, [blockCheckoutBookingStatus, submitCheckoutCommandStatus, onError, startLegacyBoxCheckoutStatus]);
88
+ }, [
89
+ blockCheckoutBookingStatus,
90
+ startLegacyBoxCheckoutStatus,
91
+ submitCheckoutCommandStatus,
92
+ checkoutBookingExpired,
93
+ onError,
94
+ ]);
71
95
  return [submitCheckout, status];
72
96
  };
73
97
  export { useSubmitCheckout };
@@ -1 +1 @@
1
- export declare const VERSION = "9.8.2";
1
+ export declare const VERSION = "9.8.3";
@@ -1 +1 @@
1
- export const VERSION = "9.8.2";
1
+ export const VERSION = "9.8.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lookiero/checkout",
3
- "version": "9.8.2",
3
+ "version": "9.8.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": "false",
package/src/ExpoRoot.tsx CHANGED
@@ -32,7 +32,7 @@ const order: Order = {
32
32
  };
33
33
 
34
34
  const customer: Customer = {
35
- customerId: "74cb4f76-3f14-4983-81a6-2ee2a9a275d4",
35
+ customerId: "d3c2354b-9811-46d4-b2ba-5fba012ed94d",
36
36
  country: Country.ES,
37
37
  segment: Segment.WOMEN,
38
38
  };
@@ -99,7 +99,7 @@ const kameleoonConfig: KameleoonEnvironment = {
99
99
  experiments: {},
100
100
  };
101
101
 
102
- const { Component: Messaging } =
102
+ const { Component: Messaging, queryBus } =
103
103
  process.env.EXPO_PUBLIC_APP_VARIANT === "test"
104
104
  ? checkoutMockBootstrap()
105
105
  : checkoutBootstrap({ apiUrl: () => apiUrl, getAuthToken });
@@ -144,6 +144,7 @@ const ExpoRoot: FC = () => {
144
144
  layout={DummyLayout}
145
145
  locale={locale}
146
146
  order={order}
147
+ queryBus={queryBus}
147
148
  subscription={subscription}
148
149
  useRedirect={useRedirect}
149
150
  onNotAccessible={onNotAccessible}
@@ -3,6 +3,7 @@ import React, { ComponentType, useCallback } from "react";
3
3
  import { Platform } from "react-native";
4
4
  import { useRoutes as reactRouterUseRoutes } from "react-router-native";
5
5
  import { I18n } from "@lookiero/i18n-react";
6
+ import { QueryBus } from "@lookiero/messaging";
6
7
  import { MessagingRoot } from "@lookiero/messaging-react/bootstrap";
7
8
  import { Locale } from "@lookiero/sty-psp-locale";
8
9
  import { SentryEnvironment, SentryLoggerFunctionArgs, sentryLogger, sentryLoggerHOC } from "@lookiero/sty-psp-logging";
@@ -11,6 +12,7 @@ import { Customer } from "../../projection/customer/customer";
11
12
  import { Order } from "../../projection/order/order";
12
13
  import { Subscription } from "../../projection/subscription/subscription";
13
14
  import { KameleoonEnvironment } from "../ab-testing/kameleoonEnvironment";
15
+ import { QueryBusProvider } from "./hooks/useQueryBus";
14
16
  import { Routing } from "./routing/Routing";
15
17
 
16
18
  interface RootFunctionArgs {
@@ -33,6 +35,7 @@ interface RootProps {
33
35
  readonly order: Order | undefined;
34
36
  readonly subscription: Subscription | undefined;
35
37
  readonly layout: Layout;
38
+ readonly queryBus: QueryBus;
36
39
  readonly onNotAccessible: () => void;
37
40
  readonly onCheckoutSubmitted?: () => void;
38
41
  readonly useRedirect: () => Record<string, string>;
@@ -51,6 +54,7 @@ const root: RootFunction = ({ Messaging, I18n, getAuthToken, development, sentry
51
54
  order,
52
55
  subscription,
53
56
  layout,
57
+ queryBus,
54
58
  onNotAccessible,
55
59
  onCheckoutSubmitted,
56
60
  useRedirect,
@@ -60,22 +64,24 @@ const root: RootFunction = ({ Messaging, I18n, getAuthToken, development, sentry
60
64
 
61
65
  return (
62
66
  <Messaging includeReactQueryDevTools={Platform.OS === "web"}>
63
- <Routing
64
- I18n={I18n}
65
- basePath={basePath}
66
- customer={customer}
67
- getAuthToken={getAuthToken}
68
- kameleoon={kameleoon}
69
- layout={layout}
70
- locale={locale}
71
- order={order}
72
- subscription={subscription}
73
- useRedirect={useRedirect}
74
- useRoutes={useRoutes}
75
- onCheckoutSubmitted={onCheckoutSubmitted}
76
- onI18nError={development ? undefined : handleOnI18nError}
77
- onNotAccessible={onNotAccessible}
78
- />
67
+ <QueryBusProvider queryBus={queryBus}>
68
+ <Routing
69
+ I18n={I18n}
70
+ basePath={basePath}
71
+ customer={customer}
72
+ getAuthToken={getAuthToken}
73
+ kameleoon={kameleoon}
74
+ layout={layout}
75
+ locale={locale}
76
+ order={order}
77
+ subscription={subscription}
78
+ useRedirect={useRedirect}
79
+ useRoutes={useRoutes}
80
+ onCheckoutSubmitted={onCheckoutSubmitted}
81
+ onI18nError={development ? undefined : handleOnI18nError}
82
+ onNotAccessible={onNotAccessible}
83
+ />
84
+ </QueryBusProvider>
79
85
  </Messaging>
80
86
  );
81
87
  };
@@ -0,0 +1,23 @@
1
+ import { renderHook, waitFor } from "@testing-library/react-native";
2
+ import { mockFn } from "jest-mock-extended";
3
+ import React, { FC } from "react";
4
+ import { QueryBus } from "@lookiero/messaging";
5
+ import { QueryBusProvider, useQueryBus as sut } from "./useQueryBus";
6
+
7
+ const mockQueryBus = mockFn<QueryBus>();
8
+
9
+ interface WrapperProps {
10
+ readonly children: JSX.Element;
11
+ }
12
+
13
+ const Wrapper: FC<WrapperProps> = ({ children }) => (
14
+ <QueryBusProvider queryBus={mockQueryBus}>{children}</QueryBusProvider>
15
+ );
16
+
17
+ describe("useQueryBus hook", () => {
18
+ it("returns the QueryBusProvider provided queryBus", async () => {
19
+ const { result } = renderHook(() => sut(), { wrapper: Wrapper });
20
+
21
+ await waitFor(() => expect(result.current).toEqual(mockQueryBus));
22
+ });
23
+ });
@@ -0,0 +1,27 @@
1
+ import React, { createContext, FC, ReactNode, useContext } from "react";
2
+ import invariant from "tiny-invariant";
3
+ import { QueryBus } from "@lookiero/messaging";
4
+
5
+ const QueryBusContext = createContext<QueryBus>(null as unknown as QueryBus);
6
+
7
+ interface QueryBusProviderProps {
8
+ readonly children: ReactNode;
9
+ readonly queryBus: QueryBus;
10
+ }
11
+
12
+ const QueryBusProvider: FC<QueryBusProviderProps> = ({ children, queryBus }) => (
13
+ <QueryBusContext.Provider value={queryBus}>{children}</QueryBusContext.Provider>
14
+ );
15
+
16
+ const useQueryBus = () => {
17
+ const queryBus = useContext(QueryBusContext);
18
+
19
+ invariant(
20
+ queryBus,
21
+ "Your are trying to use the useQueryBus hook without wrapping your app with the <QueryBusProvider>.",
22
+ );
23
+
24
+ return queryBus;
25
+ };
26
+
27
+ export { useQueryBus, QueryBusProvider };
@@ -6,9 +6,11 @@ import { PaymentFlowRef } from "@lookiero/payments-front";
6
6
  import { ChargeStatus } from "@lookiero/payments-front/build/infrastructure/CheckoutAPI";
7
7
  import { Logger } from "@lookiero/sty-psp-logging";
8
8
  import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
9
+ import { CheckoutBookingProjection } from "../../../projection/checkoutBooking/checkoutBooking";
9
10
  import { useSubmitCheckout } from "../../domain/checkout/react/useSubmitCheckout";
10
11
  import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
11
12
  import { paymentFlowPayload as mockPaymentFlowPayload } from "../../projection/payment/paymentFlowPayload.mock";
13
+ import { useQueryBus } from "./useQueryBus";
12
14
  import { useSubmitCheckout as sut } from "./useSubmitCheckout";
13
15
 
14
16
  const checkoutId = "9c450400-0cd7-44a4-b0e3-e0002a9bf8df";
@@ -18,6 +20,7 @@ const errorChargeStatuses = Object.values(ChargeStatus).filter((status) => statu
18
20
  const logger = mock<Logger>();
19
21
 
20
22
  jest.mock("@lookiero/sty-psp-notifications");
23
+ jest.mock("./useQueryBus");
21
24
  jest.mock("../../domain/checkout/react/useSubmitCheckout");
22
25
  jest.mock("../../domain/checkoutBooking/react/useBlockCheckoutBooking");
23
26
 
@@ -38,6 +41,7 @@ describe("useSubmitCheckout custom hook", () => {
38
41
  },
39
42
  };
40
43
 
44
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
41
45
  (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
42
46
  (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
43
47
  (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.SUCCESS]);
@@ -86,6 +90,7 @@ describe("useSubmitCheckout custom hook", () => {
86
90
  },
87
91
  };
88
92
 
93
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
89
94
  (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
90
95
  (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
91
96
  (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.SUCCESS]);
@@ -126,6 +131,7 @@ describe("useSubmitCheckout custom hook", () => {
126
131
  },
127
132
  };
128
133
 
134
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
129
135
  (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.ERROR]);
130
136
  (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.IDLE]);
131
137
  (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.IDLE]);
@@ -166,6 +172,7 @@ describe("useSubmitCheckout custom hook", () => {
166
172
  },
167
173
  };
168
174
 
175
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
169
176
  (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
170
177
  (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
171
178
  (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.IDLE]);
@@ -212,6 +219,7 @@ describe("useSubmitCheckout custom hook", () => {
212
219
  },
213
220
  };
214
221
 
222
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
215
223
  (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
216
224
  (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.ERROR]);
217
225
  (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
@@ -236,4 +244,54 @@ describe("useSubmitCheckout custom hook", () => {
236
244
  expect(mockOnError).toHaveBeenCalled();
237
245
  });
238
246
  });
247
+
248
+ test("returns submitCheckout error as the status right after calling 'submitCheckout' if CheckoutBooking isExpired", async () => {
249
+ const mockBlockCheckoutBooking = jest.fn();
250
+ const mockSubmitCheckout = jest.fn();
251
+ const mockCreateToastNotification = jest.fn();
252
+ const mockOnError = jest.fn();
253
+ const mockOnSuccess = jest.fn();
254
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
255
+ current: {
256
+ // eslint-disable-next-line @typescript-eslint/naming-convention
257
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
258
+ callback({ status: ChargeStatus.EXECUTED, final: true });
259
+ }),
260
+ startCheckout: jest.fn(),
261
+ },
262
+ };
263
+
264
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: true }) as CheckoutBookingProjection);
265
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
266
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
267
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.SUCCESS]);
268
+
269
+ const { result } = renderHook(() =>
270
+ sut({
271
+ checkoutId,
272
+ checkoutBookingId,
273
+ paymentFlowRef: mockPaymentFlowRef,
274
+ onError: mockOnError,
275
+ onSuccess: mockOnSuccess,
276
+ logger,
277
+ }),
278
+ );
279
+
280
+ await act(async () => {
281
+ const [submitCheckout] = result.current;
282
+ await submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: true });
283
+ });
284
+
285
+ await waitFor(() => {
286
+ const [, status] = result.current;
287
+
288
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
289
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
290
+ expect(mockSubmitCheckout).not.toHaveBeenCalled();
291
+
292
+ expect(status).toBe("error");
293
+ expect(mockOnSuccess).not.toHaveBeenCalled();
294
+ expect(mockOnError).toHaveBeenCalled();
295
+ });
296
+ });
239
297
  });
@@ -3,11 +3,17 @@ import { CommandStatus } from "@lookiero/messaging-react";
3
3
  import { PaymentFlowRef } from "@lookiero/payments-front";
4
4
  import { Logger } from "@lookiero/sty-psp-logging";
5
5
  import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
6
+ import {
7
+ ViewCheckoutBookingById,
8
+ viewCheckoutBookingById,
9
+ ViewCheckoutBookingByIdResult,
10
+ } from "../../../projection/checkoutBooking/viewCheckoutBookingById";
6
11
  import { PaymentFlowPayloadProjection } from "../../../projection/payment/paymentFlowPayload";
7
12
  import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
8
13
  import { useSubmitCheckout as useSubmitCheckoutCommand } from "../../domain/checkout/react/useSubmitCheckout";
9
14
  import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
10
15
  import { I18nMessages } from "../i18n/i18n";
16
+ import { useQueryBus } from "./useQueryBus";
11
17
 
12
18
  enum ChargeStatus {
13
19
  EXECUTED = "EXECUTED",
@@ -63,9 +69,11 @@ const useSubmitCheckout: UseSubmitCheckoutFunction = ({
63
69
  onSuccess,
64
70
  logger,
65
71
  }) => {
72
+ const queryBus = useQueryBus();
66
73
  const [submitCheckoutCommand, submitCheckoutCommandStatus] = useSubmitCheckoutCommand({ checkoutId, logger });
67
74
  const [blockCheckoutBooking, blockCheckoutBookingStatus] = useBlockCheckoutBooking({ checkoutBookingId, logger });
68
75
  const [createNotification] = useCreateToastNotification({ contextId: MESSAGING_CONTEXT_ID, logger });
76
+ const [checkoutBookingExpired, setCheckoutBookingExpired] = useState(false);
69
77
 
70
78
  const [startLegacyBoxCheckoutStatus, setStartLegacyBoxCheckoutStatus] = useState<Status>("idle");
71
79
 
@@ -77,6 +85,15 @@ const useSubmitCheckout: UseSubmitCheckoutFunction = ({
77
85
  return;
78
86
  }
79
87
 
88
+ const checkoutBooking = await queryBus<ViewCheckoutBookingById, ViewCheckoutBookingByIdResult>(
89
+ viewCheckoutBookingById({ checkoutBookingId }),
90
+ );
91
+
92
+ if (checkoutBooking?.isExpired) {
93
+ setCheckoutBookingExpired(true);
94
+ return;
95
+ }
96
+
80
97
  paymentFlowRef.current?.startLegacyBoxCheckout(
81
98
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
82
99
  // @ts-ignore
@@ -100,7 +117,15 @@ const useSubmitCheckout: UseSubmitCheckoutFunction = ({
100
117
  },
101
118
  );
102
119
  },
103
- [paymentFlowRef, blockCheckoutBooking, submitCheckoutCommand, onSuccess, createNotification],
120
+ [
121
+ queryBus,
122
+ checkoutBookingId,
123
+ paymentFlowRef,
124
+ blockCheckoutBooking,
125
+ submitCheckoutCommand,
126
+ onSuccess,
127
+ createNotification,
128
+ ],
104
129
  );
105
130
 
106
131
  const status: Status = useMemo(() => {
@@ -121,14 +146,21 @@ const useSubmitCheckout: UseSubmitCheckoutFunction = ({
121
146
  if (
122
147
  blockCheckoutBookingStatus === CommandStatus.ERROR ||
123
148
  startLegacyBoxCheckoutStatus === "error" ||
124
- submitCheckoutCommandStatus === CommandStatus.ERROR
149
+ submitCheckoutCommandStatus === CommandStatus.ERROR ||
150
+ checkoutBookingExpired
125
151
  ) {
126
152
  onError();
127
153
  return "error";
128
154
  }
129
155
 
130
156
  return "idle";
131
- }, [blockCheckoutBookingStatus, submitCheckoutCommandStatus, onError, startLegacyBoxCheckoutStatus]);
157
+ }, [
158
+ blockCheckoutBookingStatus,
159
+ startLegacyBoxCheckoutStatus,
160
+ submitCheckoutCommandStatus,
161
+ checkoutBookingExpired,
162
+ onError,
163
+ ]);
132
164
 
133
165
  return [submitCheckout, status];
134
166
  };