@lookiero/checkout 10.1.0 → 11.1.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 (80) 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/Expo.js +8 -2
  6. package/dist/src/ExpoRoot.d.ts +5 -1
  7. package/dist/src/ExpoRoot.js +26 -17
  8. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.d.ts +1 -1
  9. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.js +2 -0
  10. package/dist/src/infrastructure/projection/checkout/checkout.mock.d.ts +1 -0
  11. package/dist/src/infrastructure/projection/checkout/checkout.mock.js +3 -3
  12. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.d.ts +1 -1
  13. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.js +2 -1
  14. package/dist/src/infrastructure/tracking/tracking.d.ts +2 -2
  15. package/dist/src/infrastructure/tracking/useTrackCheckout.d.ts +10 -17
  16. package/dist/src/infrastructure/tracking/useTrackCheckout.js +27 -12
  17. package/dist/src/infrastructure/ui/Root.d.ts +6 -6
  18. package/dist/src/infrastructure/ui/Root.js +2 -3
  19. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.d.ts +26 -0
  20. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.js +127 -0
  21. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.d.ts +3 -2
  22. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.js +17 -26
  23. package/dist/src/infrastructure/ui/i18n/i18n.d.ts +1 -0
  24. package/dist/src/infrastructure/ui/i18n/i18n.js +1 -0
  25. package/dist/src/infrastructure/ui/routing/CheckoutMiddleware.js +1 -12
  26. package/dist/src/infrastructure/ui/routing/Routing.d.ts +5 -5
  27. package/dist/src/infrastructure/ui/routing/Routing.js +2 -10
  28. package/dist/src/infrastructure/ui/routing/routes.d.ts +0 -1
  29. package/dist/src/infrastructure/ui/routing/routes.js +0 -1
  30. package/dist/src/infrastructure/ui/views/App.js +5 -6
  31. package/dist/src/infrastructure/ui/views/checkout/Checkout.d.ts +7 -2
  32. package/dist/src/infrastructure/ui/views/checkout/Checkout.js +20 -9
  33. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.d.ts +3 -0
  34. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.js +3 -0
  35. package/dist/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.js +7 -7
  36. package/dist/src/projection/customer/customer.d.ts +2 -0
  37. package/dist/src/projection/order/order.d.ts +1 -1
  38. package/dist/src/projection/subscription/subscription.d.ts +1 -1
  39. package/dist/src/version.d.ts +1 -1
  40. package/dist/src/version.js +1 -1
  41. package/fake-dependencies/@lookiero/payments-front/index.tsx +36 -8
  42. package/index.ts +10 -3
  43. package/package.json +3 -4
  44. package/src/Expo.tsx +10 -2
  45. package/src/ExpoRoot.tsx +58 -43
  46. package/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.ts +4 -1
  47. package/src/infrastructure/projection/checkout/checkout.mock.ts +8 -3
  48. package/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.ts +3 -2
  49. package/src/infrastructure/tracking/tracking.ts +2 -2
  50. package/src/infrastructure/tracking/useTrackCheckout.test.tsx +51 -24
  51. package/src/infrastructure/tracking/useTrackCheckout.ts +66 -56
  52. package/src/infrastructure/ui/Root.tsx +9 -9
  53. package/src/infrastructure/ui/components/templates/header/itemHeader/ItemHeader.tsx +1 -0
  54. package/src/infrastructure/ui/hooks/useCheckoutFlow.test.tsx +302 -0
  55. package/src/infrastructure/ui/hooks/useCheckoutFlow.tsx +203 -0
  56. package/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.ts +18 -60
  57. package/src/infrastructure/ui/i18n/i18n.ts +1 -0
  58. package/src/infrastructure/ui/routing/CheckoutMiddleware.test.tsx +0 -11
  59. package/src/infrastructure/ui/routing/CheckoutMiddleware.tsx +1 -15
  60. package/src/infrastructure/ui/routing/Routing.tsx +14 -25
  61. package/src/infrastructure/ui/routing/routes.ts +0 -1
  62. package/src/infrastructure/ui/views/App.tsx +5 -13
  63. package/src/infrastructure/ui/views/checkout/Checkout.style.ts +3 -0
  64. package/src/infrastructure/ui/views/checkout/Checkout.test.tsx +51 -43
  65. package/src/infrastructure/ui/views/checkout/Checkout.tsx +51 -13
  66. package/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.tsx +8 -8
  67. package/src/infrastructure/ui/views/item/components/itemActions/__snapshots__/ItemActions.test.tsx.snap +8 -0
  68. package/src/infrastructure/ui/views/item/components/selectModal/__snapshots__/SelecModal.test.tsx.snap +8 -0
  69. package/src/infrastructure/ui/views/item/components/sizeWithoutStockModal/__snapshots__/SizeWithoutStockModal.test.tsx.snap +8 -0
  70. package/src/projection/customer/customer.ts +2 -0
  71. package/src/projection/order/order.ts +1 -1
  72. package/src/projection/subscription/subscription.ts +1 -1
  73. package/dist/src/infrastructure/ui/hooks/useSubmitCheckout.d.ts +0 -27
  74. package/dist/src/infrastructure/ui/hooks/useSubmitCheckout.js +0 -97
  75. package/dist/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.d.ts +0 -12
  76. package/dist/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.js +0 -88
  77. package/src/infrastructure/ui/hooks/useSubmitCheckout.test.ts +0 -297
  78. package/src/infrastructure/ui/hooks/useSubmitCheckout.ts +0 -169
  79. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.test.tsx +0 -134
  80. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.tsx +0 -124
@@ -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
+ });
@@ -0,0 +1,203 @@
1
+ import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { CommandStatus } from "@lookiero/messaging-react";
3
+ import { PaymentFlow, PaymentFlowRef, PaymentPayload, Section } from "@lookiero/payments-front";
4
+ import { LegacyBoxCheckoutStrategyPayload } from "@lookiero/payments-front/build/components/PaymentFlow/internals/strategies/LegacyBoxCheckoutStrategy";
5
+ import { useLogger } from "@lookiero/sty-psp-logging";
6
+ import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
7
+ import { CheckoutProjection } from "../../../projection/checkout/checkout";
8
+ import {
9
+ ViewCheckoutBookingById,
10
+ viewCheckoutBookingById,
11
+ ViewCheckoutBookingByIdResult,
12
+ } from "../../../projection/checkoutBooking/viewCheckoutBookingById";
13
+ import { OrderProjection } from "../../../projection/order/order";
14
+ import { SubscriptionProjection } from "../../../projection/subscription/subscription";
15
+ import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
16
+ import { useSubmitCheckout } from "../../domain/checkout/react/useSubmitCheckout";
17
+ import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
18
+ import { useViewIsSizeChangeEnabledByCheckoutId } from "../../projection/checkout/react/useViewIsSizeChangeEnabledByCheckoutId";
19
+ import { useViewPaymentFlowPayloadByCheckoutId } from "../../projection/payment/react/useViewPaymentFlowPayloadByCheckoutId";
20
+ import { useViewPricingByCheckoutId } from "../../projection/pricing/react/useViewPricingByCheckoutId";
21
+ import { useTrackCheckout } from "../../tracking/useTrackCheckout";
22
+ import { I18nMessages } from "../i18n/i18n";
23
+ import { Routes } from "../routing/routes";
24
+ import { usePaymentInstrumentEvents } from "./usePaymentInstrumentEvents";
25
+ import { useQueryBus } from "./useQueryBus";
26
+ import { useStaticInfo } from "./useStaticInfo";
27
+
28
+ type CheckoutFlowStatus = "idle" | "loading" | "success" | "error";
29
+
30
+ interface CheckoutFlowFunction {
31
+ (): Promise<void>;
32
+ }
33
+
34
+ type CheckoutFlowReturn = [
35
+ checkoutFlow: CheckoutFlowFunction,
36
+ checkoutFlowStatus: CheckoutFlowStatus,
37
+ paymentFlow: ReactNode,
38
+ ];
39
+
40
+ interface UseCheckoutFlowArgs {
41
+ readonly checkout: CheckoutProjection | undefined;
42
+ readonly order: OrderProjection;
43
+ readonly subscription: SubscriptionProjection;
44
+ readonly getAuthToken: () => Promise<string>;
45
+ readonly onSuccess: () => void;
46
+ }
47
+
48
+ interface UseCheckoutFlowFunction {
49
+ (args: UseCheckoutFlowArgs): CheckoutFlowReturn;
50
+ }
51
+
52
+ const useCheckoutFlow: UseCheckoutFlowFunction = ({
53
+ checkout: checkoutProjection,
54
+ order: orderProjection,
55
+ subscription: subscriptionProjection,
56
+ getAuthToken,
57
+ onSuccess,
58
+ }) => {
59
+ const logger = useLogger();
60
+ const queryBus = useQueryBus();
61
+ const {
62
+ customer: { customerId, country, segment, name, email },
63
+ basePath,
64
+ } = useStaticInfo();
65
+ const paymentFlowRef = useRef<PaymentFlowRef>(null);
66
+ const [paymentFlowPayload] = useViewPaymentFlowPayloadByCheckoutId({
67
+ checkoutId: checkoutProjection?.id as string,
68
+ });
69
+ const [sizeChangeEnabled] = useViewIsSizeChangeEnabledByCheckoutId({ checkoutId: checkoutProjection?.id as string });
70
+ const [pricing] = useViewPricingByCheckoutId({
71
+ checkoutId: checkoutProjection?.id,
72
+ queryOptions: { refetchOnMount: true },
73
+ });
74
+ const [submitCheckout, submitCheckoutStatus] = useSubmitCheckout({
75
+ checkoutId: checkoutProjection?.id,
76
+ logger,
77
+ });
78
+ const [blockCheckoutBooking, blockCheckoutBookingStatus] = useBlockCheckoutBooking({
79
+ checkoutBookingId: checkoutProjection?.checkoutBookingId,
80
+ logger,
81
+ });
82
+ const [createNotification] = useCreateToastNotification({ contextId: MESSAGING_CONTEXT_ID, logger });
83
+ const [checkoutBookingExpired, setCheckoutBookingExpired] = useState(false);
84
+ const [startLegacyBoxCheckoutStatus, setStartLegacyBoxCheckoutStatus] = useState<CheckoutFlowStatus>("idle");
85
+ const [authToken, setAuthToken] = useState<string>();
86
+ useEffect(() => {
87
+ const loadAuthToken = async () => setAuthToken(await getAuthToken());
88
+ loadAuthToken();
89
+ }, [getAuthToken]);
90
+ const trackCheckout = useTrackCheckout({
91
+ checkout: checkoutProjection,
92
+ order: orderProjection,
93
+ pricing,
94
+ subscription: subscriptionProjection,
95
+ userId: customerId,
96
+ country,
97
+ segment,
98
+ });
99
+
100
+ const checkoutFlow: CheckoutFlowFunction = useCallback(async () => {
101
+ try {
102
+ sizeChangeEnabled && (await blockCheckoutBooking());
103
+ } catch (error) {
104
+ return;
105
+ }
106
+
107
+ if (checkoutProjection?.checkoutBookingId) {
108
+ const checkoutBooking = await queryBus<ViewCheckoutBookingById, ViewCheckoutBookingByIdResult>(
109
+ viewCheckoutBookingById({ checkoutBookingId: checkoutProjection?.checkoutBookingId }),
110
+ );
111
+
112
+ if (checkoutBooking?.isExpired) {
113
+ setCheckoutBookingExpired(true);
114
+ return;
115
+ }
116
+ }
117
+
118
+ setStartLegacyBoxCheckoutStatus("loading");
119
+
120
+ paymentFlowRef.current?.startLegacyBoxCheckout({
121
+ ...paymentFlowPayload,
122
+ userInformation: { email, name },
123
+ returnUrl: `${basePath}/${Routes.CHECKOUT}`,
124
+ } as unknown as LegacyBoxCheckoutStrategyPayload);
125
+ }, [
126
+ checkoutProjection?.checkoutBookingId,
127
+ paymentFlowPayload,
128
+ email,
129
+ name,
130
+ basePath,
131
+ sizeChangeEnabled,
132
+ blockCheckoutBooking,
133
+ queryBus,
134
+ ]);
135
+
136
+ const onPaymentSuccess = useCallback(async () => {
137
+ setStartLegacyBoxCheckoutStatus("success");
138
+
139
+ await submitCheckout();
140
+
141
+ createNotification({
142
+ bodyI18nKey: I18nMessages.CHECKOUT_TOAST_PAYMENT_SUCCESS,
143
+ level: NotificationLevel.SUCCESS,
144
+ });
145
+
146
+ trackCheckout();
147
+ onSuccess();
148
+ }, [createNotification, onSuccess, submitCheckout, trackCheckout]);
149
+ const onPaymentError = useCallback(
150
+ (payload: PaymentPayload) => {
151
+ setStartLegacyBoxCheckoutStatus("error");
152
+
153
+ createNotification({
154
+ bodyI18nKey: payload.metadata?.toaster?.id || I18nMessages.CHECKOUT_TOAST_PAYMENT_ERROR,
155
+ level: NotificationLevel.ERROR,
156
+ });
157
+ },
158
+ [createNotification],
159
+ );
160
+ usePaymentInstrumentEvents({ onSuccess: onPaymentSuccess, onError: onPaymentError });
161
+
162
+ const checkoutFlowStatus: CheckoutFlowStatus = useMemo(() => {
163
+ if (
164
+ blockCheckoutBookingStatus === CommandStatus.LOADING ||
165
+ startLegacyBoxCheckoutStatus === "loading" ||
166
+ submitCheckoutStatus === CommandStatus.LOADING
167
+ ) {
168
+ return "loading";
169
+ }
170
+
171
+ if (
172
+ blockCheckoutBookingStatus === CommandStatus.SUCCESS &&
173
+ startLegacyBoxCheckoutStatus === "success" &&
174
+ submitCheckoutStatus === CommandStatus.SUCCESS
175
+ ) {
176
+ return "success";
177
+ }
178
+
179
+ if (
180
+ blockCheckoutBookingStatus === CommandStatus.ERROR ||
181
+ startLegacyBoxCheckoutStatus === "error" ||
182
+ submitCheckoutStatus === CommandStatus.ERROR ||
183
+ checkoutBookingExpired
184
+ ) {
185
+ return "error";
186
+ }
187
+
188
+ return "idle";
189
+ }, [blockCheckoutBookingStatus, startLegacyBoxCheckoutStatus, submitCheckoutStatus, checkoutBookingExpired]);
190
+
191
+ const paymentFlow = useMemo(
192
+ () => (authToken ? <PaymentFlow ref={paymentFlowRef} section={Section.BOX_CHECKOUT} token={authToken} /> : null),
193
+ [authToken],
194
+ );
195
+
196
+ return useMemo(
197
+ () => [checkoutFlow, checkoutFlowStatus, paymentFlow],
198
+ [checkoutFlow, paymentFlow, checkoutFlowStatus],
199
+ );
200
+ };
201
+
202
+ export type { CheckoutFlowStatus };
203
+ export { useCheckoutFlow };
@@ -1,75 +1,33 @@
1
- import { useCallback, useEffect } from "react";
2
- import { useEvent } from "@lookiero/event";
3
- import { Logger } from "@lookiero/sty-psp-logging";
4
- import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
5
- import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
6
- import { I18nMessages } from "../i18n/i18n";
7
-
8
- const PAYMENT_ERROR = "ERROR";
9
- const PAYMENT_SUCCESS = "PAYMENT_INSTRUMENT_UPDATED";
10
-
11
- interface Message {
12
- readonly id: string;
13
- }
14
- interface OnSuccessFunctionArgs {
15
- readonly message: Message;
16
- }
17
- interface OnSuccessFunction {
18
- (args: OnSuccessFunctionArgs): void;
19
- }
20
-
21
- interface Toaster {
22
- readonly id: string;
23
- }
24
- interface Error {
25
- readonly toaster?: Toaster;
26
- }
27
- interface OnErrorFunctionArgs {
28
- readonly error: Error;
29
- }
30
- interface OnErrorFunction {
31
- (args: OnErrorFunctionArgs): void;
32
- }
1
+ import { useEffect } from "react";
2
+ import { PaymentPayload, Section, usePaymentStatusManager } from "@lookiero/payments-front";
33
3
 
34
4
  interface UsePaymentInstrumentEventsFunctionArgs {
35
- readonly logger: Logger;
5
+ readonly onSuccess: (payload: PaymentPayload) => void;
6
+ readonly onError: (payload: PaymentPayload) => void;
36
7
  }
37
8
 
38
9
  interface UsePaymentInstrumentEventsFunction {
39
10
  (args: UsePaymentInstrumentEventsFunctionArgs): void;
40
11
  }
41
12
 
42
- const usePaymentInstrumentEvents: UsePaymentInstrumentEventsFunction = ({ logger }) => {
43
- const { subscribe, unsubscribe } = useEvent();
13
+ const usePaymentInstrumentEvents: UsePaymentInstrumentEventsFunction = ({ onSuccess, onError }) => {
14
+ const refreshStatus = usePaymentStatusManager(Section.BOX_CHECKOUT);
44
15
 
45
- const [createNotification] = useCreateToastNotification({ contextId: MESSAGING_CONTEXT_ID, logger });
16
+ useEffect(() => {
17
+ const { isLoading, consumePayload } = refreshStatus;
46
18
 
47
- const onSuccess: OnSuccessFunction = useCallback(
48
- ({ message }) => createNotification({ bodyI18nKey: message.id, level: NotificationLevel.SUCCESS }),
49
- [createNotification],
50
- );
19
+ if (isLoading) {
20
+ return;
21
+ }
51
22
 
52
- const onError: OnErrorFunction = useCallback(
53
- ({ error }) => {
54
- if (error.toaster) {
55
- createNotification({
56
- bodyI18nKey: error.toaster.id || I18nMessages.CHECKOUT_TOAST_PAYMENT_ERROR,
57
- level: NotificationLevel.ERROR,
58
- });
23
+ consumePayload((payload) => {
24
+ if (payload.success) {
25
+ onSuccess(payload);
26
+ } else {
27
+ onError(payload);
59
28
  }
60
- },
61
- [createNotification],
62
- );
63
-
64
- useEffect(() => {
65
- subscribe({ event: PAYMENT_ERROR }, onError);
66
- subscribe({ event: PAYMENT_SUCCESS }, onSuccess);
67
-
68
- return () => {
69
- unsubscribe({ event: PAYMENT_ERROR }, onError);
70
- unsubscribe({ event: PAYMENT_SUCCESS }, onSuccess);
71
- };
72
- }, [subscribe, unsubscribe, createNotification, onError, onSuccess]);
29
+ });
30
+ }, [onError, onSuccess, refreshStatus]);
73
31
  };
74
32
 
75
33
  export { usePaymentInstrumentEvents };
@@ -50,6 +50,7 @@ enum I18nMessages {
50
50
  CHECKOUT_TITLE = "checkout.title",
51
51
  CHECKOUT_PAY_BUTTON = "checkout.pay_button",
52
52
  CHECKOUT_TOAST_PAYMENT_ERROR = "checkout.toast_payment_error",
53
+ CHECKOUT_TOAST_PAYMENT_SUCCESS = "checkout.toast_payment_success",
53
54
  CHECKOUT_SUCCESS_MODAL_TITLE = "checkout.success_modal_title",
54
55
  CHECKOUT_SUCCESS_MODAL_DESCRIPTION = "checkout.success_modal_description",
55
56
  CHECKOUT_SUCCESS_MODAL_BUTTON = "checkout.success_modal_button",
@@ -158,17 +158,6 @@ describe("CheckoutMiddleware component", () => {
158
158
  expect(onNotAccessible).toHaveBeenCalled();
159
159
  });
160
160
 
161
- it("payment route should not be directly accessible", async () => {
162
- const checkoutMock = checkout({ items: [{ status: CheckoutItemStatus.INITIAL }] });
163
- (useViewFirstAvailableCheckoutByCustomerId as jest.Mock).mockReturnValue([checkoutMock, QueryStatus.SUCCESS]);
164
- (useStartCheckout as jest.Mock).mockReturnValue([mockStartCheckout, CommandStatus.SUCCESS]);
165
-
166
- const onNotAccessible = jest.fn();
167
- renderCheckoutMiddleware({ path: `${Routes.CHECKOUT}/${Routes.CHECKOUT_PAYMENT}`, onNotAccessible });
168
-
169
- expect(onNotAccessible).toHaveBeenCalled();
170
- });
171
-
172
161
  it("should redirect to 'feedback' if checkout's status is SUBMITTED", async () => {
173
162
  const checkoutMock = checkout({
174
163
  status: CheckoutStatus.SUBMITTED,
@@ -48,12 +48,6 @@ const CheckoutMiddleware: FC<CheckoutMiddlewareProps> = ({
48
48
  const feedbackRouteMatch = useMatch(`${basePath}/${Routes.FEEDBACK}`);
49
49
  const feedbackRouteMatchRef = useRef(feedbackRouteMatch);
50
50
  feedbackRouteMatchRef.current = feedbackRouteMatch;
51
- const checkoutPaymentRouteMatch = useMatch(`${basePath}/${Routes.CHECKOUT}/${Routes.CHECKOUT_PAYMENT}`);
52
- const checkoutPaymentRouteMatchRef = useRef(checkoutPaymentRouteMatch);
53
- checkoutPaymentRouteMatchRef.current = checkoutPaymentRouteMatch;
54
-
55
- const checkoutShown = useRef(false);
56
- checkoutShown.current = checkoutShown.current || (Boolean(checkoutRouteMatch) && !Boolean(checkoutPaymentRouteMatch));
57
51
 
58
52
  const [checkout] = useViewFirstAvailableCheckoutByCustomerId({ customerId });
59
53
  const checkoutItemsRef = useRef<CheckoutItemProjection[]>();
@@ -99,8 +93,7 @@ const CheckoutMiddleware: FC<CheckoutMiddlewareProps> = ({
99
93
  summaryRouteMatchRef.current ||
100
94
  summaryTabsRouteMatchRef.current ||
101
95
  itemDetailRouteMatchRef.current ||
102
- checkoutRouteMatchRef.current ||
103
- checkoutPaymentRouteMatchRef.current
96
+ checkoutRouteMatchRef.current
104
97
  )
105
98
  ) {
106
99
  navigateRef.current(`${basePath}/${Routes.SUMMARY}`, { replace: true });
@@ -127,13 +120,6 @@ const CheckoutMiddleware: FC<CheckoutMiddlewareProps> = ({
127
120
  return null;
128
121
  }
129
122
 
130
- /* Prevent direct payment access */
131
- if (checkoutPaymentRouteMatch && !checkoutShown.current) {
132
- onNotAccessible();
133
-
134
- return null;
135
- }
136
-
137
123
  return children;
138
124
  };
139
125