@lookiero/checkout 10.0.0-beta.4 → 10.0.1

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 (83) hide show
  1. package/dist/fake-dependencies/@lookiero/payments-front/index.d.ts +6 -8
  2. package/dist/fake-dependencies/@lookiero/payments-front/index.js +4 -7
  3. package/dist/index.d.ts +5 -4
  4. package/dist/index.js +2 -1
  5. package/dist/src/ExpoRoot.js +13 -17
  6. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.d.ts +1 -1
  7. package/dist/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.js +0 -2
  8. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.d.ts +1 -1
  9. package/dist/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.js +1 -2
  10. package/dist/src/infrastructure/tracking/tracking.d.ts +2 -2
  11. package/dist/src/infrastructure/tracking/useTrackCheckout.d.ts +17 -10
  12. package/dist/src/infrastructure/tracking/useTrackCheckout.js +12 -27
  13. package/dist/src/infrastructure/ui/Root.d.ts +9 -7
  14. package/dist/src/infrastructure/ui/Root.js +3 -2
  15. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.d.ts +2 -3
  16. package/dist/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.js +26 -17
  17. package/dist/src/infrastructure/ui/hooks/useStaticInfo.d.ts +3 -0
  18. package/dist/src/infrastructure/ui/hooks/useStaticInfo.js +7 -2
  19. package/dist/src/infrastructure/ui/i18n/i18n.d.ts +0 -1
  20. package/dist/src/infrastructure/ui/i18n/i18n.js +0 -1
  21. package/dist/src/infrastructure/ui/routing/CheckoutMiddleware.js +12 -1
  22. package/dist/src/infrastructure/ui/routing/Routing.d.ts +7 -5
  23. package/dist/src/infrastructure/ui/routing/Routing.js +11 -3
  24. package/dist/src/infrastructure/ui/routing/routes.d.ts +1 -0
  25. package/dist/src/infrastructure/ui/routing/routes.js +1 -0
  26. package/dist/src/infrastructure/ui/views/App.js +6 -5
  27. package/dist/src/infrastructure/ui/views/checkout/Checkout.d.ts +2 -7
  28. package/dist/src/infrastructure/ui/views/checkout/Checkout.js +9 -20
  29. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.d.ts +0 -3
  30. package/dist/src/infrastructure/ui/views/checkout/Checkout.style.js +0 -3
  31. package/dist/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.js +1 -3
  32. package/dist/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.js +7 -7
  33. package/dist/src/infrastructure/ui/views/summary/Summary.js +1 -2
  34. package/dist/src/projection/customer/customer.d.ts +0 -2
  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 +9 -32
  40. package/index.ts +5 -11
  41. package/package.json +4 -3
  42. package/src/ExpoRoot.tsx +37 -42
  43. package/src/infrastructure/domain/checkoutBooking/react/useBlockCheckoutBooking.ts +1 -4
  44. package/src/infrastructure/projection/pricing/react/useViewPricingByCheckoutId.ts +2 -3
  45. package/src/infrastructure/tracking/tracking.ts +2 -2
  46. package/src/infrastructure/tracking/useTrackCheckout.ts +56 -66
  47. package/src/infrastructure/ui/Root.tsx +14 -10
  48. package/src/infrastructure/ui/components/templates/header/itemHeader/ItemHeader.test.tsx +6 -0
  49. package/src/infrastructure/ui/components/templates/header/itemHeader/ItemHeader.tsx +0 -1
  50. package/src/infrastructure/ui/hooks/usePaymentInstrumentEvents.ts +60 -18
  51. package/src/infrastructure/ui/hooks/useStaticInfo.test.tsx +6 -1
  52. package/src/infrastructure/ui/hooks/useStaticInfo.tsx +13 -2
  53. package/src/infrastructure/ui/hooks/useSubmitCheckout.test.ts +297 -0
  54. package/src/infrastructure/ui/hooks/useSubmitCheckout.ts +169 -0
  55. package/src/infrastructure/ui/i18n/i18n.ts +0 -1
  56. package/src/infrastructure/ui/routing/CheckoutMiddleware.test.tsx +9 -1
  57. package/src/infrastructure/ui/routing/CheckoutMiddleware.tsx +15 -1
  58. package/src/infrastructure/ui/routing/Routing.tsx +29 -15
  59. package/src/infrastructure/ui/routing/routes.ts +1 -0
  60. package/src/infrastructure/ui/views/App.tsx +13 -5
  61. package/src/infrastructure/ui/views/checkout/Checkout.style.ts +0 -3
  62. package/src/infrastructure/ui/views/checkout/Checkout.test.tsx +43 -51
  63. package/src/infrastructure/ui/views/checkout/Checkout.tsx +13 -51
  64. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.test.tsx +134 -0
  65. package/src/infrastructure/ui/views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal.tsx +124 -0
  66. package/src/infrastructure/ui/views/checkout/components/paymentInstrument/PaymentInstrument.tsx +8 -8
  67. package/src/infrastructure/ui/views/return/components/returnQuestionsForm/ReturnQuestionsForm.test.tsx +6 -0
  68. package/src/infrastructure/ui/views/summary/Summary.tsx +1 -1
  69. package/src/infrastructure/ui/views/summaryTabs/SummaryTabs.test.tsx +4 -1
  70. package/src/projection/customer/customer.ts +0 -2
  71. package/src/projection/order/order.ts +1 -1
  72. package/src/projection/subscription/subscription.ts +1 -1
  73. package/dist/public/public/assets/adaptive-icon.png +0 -0
  74. package/dist/public/public/assets/favicon.png +0 -0
  75. package/dist/public/public/assets/icon.png +0 -0
  76. package/dist/public/public/assets/splash.png +0 -0
  77. package/dist/public/public/images/not-found.png +0 -0
  78. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.d.ts +0 -26
  79. package/dist/src/infrastructure/ui/hooks/useCheckoutFlow.js +0 -127
  80. package/dist/src/infrastructure/ui/routing/useBasePath.d.ts +0 -8
  81. package/dist/src/infrastructure/ui/routing/useBasePath.js +0 -9
  82. package/src/infrastructure/ui/hooks/useCheckoutFlow.test.tsx +0 -302
  83. package/src/infrastructure/ui/hooks/useCheckoutFlow.tsx +0 -203
@@ -0,0 +1,297 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react-native";
2
+ import { mock } from "jest-mock-extended";
3
+ import { RefObject } from "react";
4
+ import { CommandStatus } from "@lookiero/messaging-react";
5
+ import { PaymentFlowRef } from "@lookiero/payments-front";
6
+ import { ChargeStatus } from "@lookiero/payments-front/build/infrastructure/CheckoutAPI";
7
+ import { Logger } from "@lookiero/sty-psp-logging";
8
+ import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
9
+ import { CheckoutBookingProjection } from "../../../projection/checkoutBooking/checkoutBooking";
10
+ import { useSubmitCheckout } from "../../domain/checkout/react/useSubmitCheckout";
11
+ import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
12
+ import { paymentFlowPayload as mockPaymentFlowPayload } from "../../projection/payment/paymentFlowPayload.mock";
13
+ import { useQueryBus } from "./useQueryBus";
14
+ import { useSubmitCheckout as sut } from "./useSubmitCheckout";
15
+
16
+ const checkoutId = "9c450400-0cd7-44a4-b0e3-e0002a9bf8df";
17
+ const checkoutBookingId = "07c996bb-a0b4-45f9-ae21-6bb9c784d12b";
18
+ const errorChargeStatuses = Object.values(ChargeStatus).filter((status) => status !== ChargeStatus.EXECUTED);
19
+
20
+ const logger = mock<Logger>();
21
+
22
+ jest.mock("@lookiero/sty-psp-notifications");
23
+ jest.mock("./useQueryBus");
24
+ jest.mock("../../domain/checkout/react/useSubmitCheckout");
25
+ jest.mock("../../domain/checkoutBooking/react/useBlockCheckoutBooking");
26
+
27
+ describe("useSubmitCheckout custom hook", () => {
28
+ test("returns success as the status right after calling 'submitCheckout'", async () => {
29
+ const mockBlockCheckoutBooking = jest.fn();
30
+ const mockSubmitCheckout = jest.fn();
31
+ const mockCreateToastNotification = jest.fn();
32
+ const mockOnError = jest.fn();
33
+ const mockOnSuccess = jest.fn();
34
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
35
+ current: {
36
+ // eslint-disable-next-line @typescript-eslint/naming-convention
37
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
38
+ callback({ status: ChargeStatus.EXECUTED, final: true });
39
+ }),
40
+ startCheckout: jest.fn(),
41
+ },
42
+ };
43
+
44
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
45
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
46
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
47
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.SUCCESS]);
48
+
49
+ const { result } = renderHook(() =>
50
+ sut({
51
+ checkoutId,
52
+ checkoutBookingId,
53
+ paymentFlowRef: mockPaymentFlowRef,
54
+ onError: mockOnError,
55
+ onSuccess: mockOnSuccess,
56
+ logger,
57
+ }),
58
+ );
59
+
60
+ await act(async () => {
61
+ const [submitCheckout] = result.current;
62
+ await submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: true });
63
+ });
64
+
65
+ await waitFor(() => {
66
+ const [, status] = result.current;
67
+
68
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
69
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
70
+ expect(mockSubmitCheckout).toHaveBeenCalled();
71
+
72
+ expect(status).toBe("success");
73
+ expect(mockOnSuccess).toHaveBeenCalled();
74
+ expect(mockOnError).not.toHaveBeenCalled();
75
+ });
76
+ });
77
+
78
+ test("does not call blockCheckoutBooking if sizeChange is not enabled after calling 'submitCheckout'", async () => {
79
+ const mockBlockCheckoutBooking = jest.fn();
80
+ const mockSubmitCheckout = jest.fn();
81
+ const mockCreateToastNotification = jest.fn();
82
+ const mockOnError = jest.fn();
83
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
84
+ current: {
85
+ // eslint-disable-next-line @typescript-eslint/naming-convention
86
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
87
+ callback({ status: ChargeStatus.EXECUTED, final: true });
88
+ }),
89
+ startCheckout: jest.fn(),
90
+ },
91
+ };
92
+
93
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
94
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
95
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
96
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.SUCCESS]);
97
+
98
+ const { result } = renderHook(() =>
99
+ sut({ checkoutId, checkoutBookingId, paymentFlowRef: mockPaymentFlowRef, onError: mockOnError, logger }),
100
+ );
101
+
102
+ await act(async () => {
103
+ const [submitCheckout] = result.current;
104
+ await submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: false });
105
+ });
106
+
107
+ await waitFor(() => {
108
+ const [, status] = result.current;
109
+
110
+ expect(mockBlockCheckoutBooking).not.toHaveBeenCalled();
111
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
112
+ expect(mockSubmitCheckout).toHaveBeenCalled();
113
+
114
+ expect(status).toBe("success");
115
+ expect(mockOnError).not.toHaveBeenCalled();
116
+ });
117
+ });
118
+
119
+ test("returns blockCheckoutBooking error as the status right after calling 'submitCheckout'", async () => {
120
+ const mockBlockCheckoutBooking = jest.fn();
121
+ const mockSubmitCheckout = jest.fn();
122
+ const mockCreateToastNotification = jest.fn();
123
+ const mockOnError = jest.fn();
124
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
125
+ current: {
126
+ // eslint-disable-next-line @typescript-eslint/naming-convention
127
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
128
+ callback({ status: ChargeStatus.EXECUTED, final: true });
129
+ }),
130
+ startCheckout: jest.fn(),
131
+ },
132
+ };
133
+
134
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
135
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.ERROR]);
136
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.IDLE]);
137
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.IDLE]);
138
+
139
+ const { result } = renderHook(() =>
140
+ sut({ checkoutId, checkoutBookingId, paymentFlowRef: mockPaymentFlowRef, onError: mockOnError, logger }),
141
+ );
142
+
143
+ await act(async () => {
144
+ const [submitCheckout] = result.current;
145
+ await submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: true });
146
+ });
147
+
148
+ await waitFor(() => {
149
+ const [, status] = result.current;
150
+
151
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
152
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
153
+ expect(status).toBe("error");
154
+ expect(mockOnError).toHaveBeenCalled();
155
+ });
156
+ });
157
+
158
+ test.each(errorChargeStatuses)(
159
+ "shows an 'error' notification when the checkout-flow fails (status %s)",
160
+ async (chargeStatus) => {
161
+ const mockBlockCheckoutBooking = jest.fn();
162
+ const mockSubmitCheckout = jest.fn();
163
+ const mockCreateToastNotification = jest.fn();
164
+ const mockOnError = jest.fn();
165
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
166
+ current: {
167
+ // eslint-disable-next-line @typescript-eslint/naming-convention
168
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
169
+ callback({ status: chargeStatus, toaster: { id: `${chargeStatus}_toaster_id` }, final: true });
170
+ }),
171
+ startCheckout: jest.fn(),
172
+ },
173
+ };
174
+
175
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
176
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
177
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
178
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.IDLE]);
179
+
180
+ const { result } = renderHook(() =>
181
+ sut({ checkoutId, checkoutBookingId, paymentFlowRef: mockPaymentFlowRef, onError: mockOnError, logger }),
182
+ );
183
+
184
+ await act(async () => {
185
+ const [submitCheckout] = result.current;
186
+ await submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: true });
187
+ });
188
+
189
+ await waitFor(() => {
190
+ const [, status] = result.current;
191
+
192
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
193
+ expect(mockCreateToastNotification).toHaveBeenCalledWith(
194
+ expect.objectContaining({
195
+ level: NotificationLevel.ERROR,
196
+ bodyI18nKey: `${chargeStatus}_toaster_id`,
197
+ }),
198
+ );
199
+ expect(mockSubmitCheckout).not.toHaveBeenCalled();
200
+
201
+ expect(status).toBe("error");
202
+ expect(mockOnError).toHaveBeenCalled();
203
+ });
204
+ },
205
+ );
206
+
207
+ test("returns submitCheckout error as the status right after calling 'submitCheckout'", async () => {
208
+ const mockBlockCheckoutBooking = jest.fn();
209
+ const mockSubmitCheckout = jest.fn();
210
+ const mockCreateToastNotification = jest.fn();
211
+ const mockOnError = jest.fn();
212
+ const mockPaymentFlowRef: RefObject<PaymentFlowRef> = {
213
+ current: {
214
+ // eslint-disable-next-line @typescript-eslint/naming-convention
215
+ startLegacyBoxCheckout: jest.fn().mockImplementation((_payload, callback) => {
216
+ callback({ status: ChargeStatus.EXECUTED, final: true });
217
+ }),
218
+ startCheckout: jest.fn(),
219
+ },
220
+ };
221
+
222
+ (useQueryBus as jest.Mock).mockReturnValue(() => ({ isExpired: false }) as CheckoutBookingProjection);
223
+ (useBlockCheckoutBooking as jest.Mock).mockReturnValue([mockBlockCheckoutBooking, CommandStatus.SUCCESS]);
224
+ (useSubmitCheckout as jest.Mock).mockReturnValue([mockSubmitCheckout, CommandStatus.ERROR]);
225
+ (useCreateToastNotification as jest.Mock).mockReturnValue([mockCreateToastNotification, CommandStatus.SUCCESS]);
226
+
227
+ const { result } = renderHook(() =>
228
+ sut({ checkoutId, checkoutBookingId, paymentFlowRef: mockPaymentFlowRef, onError: mockOnError, logger }),
229
+ );
230
+
231
+ await act(async () => {
232
+ const [submitCheckout] = result.current;
233
+ submitCheckout({ paymentFlowPayload: mockPaymentFlowPayload, sizeChangeEnabled: true });
234
+ });
235
+
236
+ await waitFor(() => {
237
+ const [, status] = result.current;
238
+
239
+ expect(mockBlockCheckoutBooking).toHaveBeenCalled();
240
+ expect(mockCreateToastNotification).not.toHaveBeenCalled();
241
+ expect(mockSubmitCheckout).toHaveBeenCalled();
242
+
243
+ expect(status).toBe("error");
244
+ expect(mockOnError).toHaveBeenCalled();
245
+ });
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
+ });
297
+ });
@@ -0,0 +1,169 @@
1
+ import { RefObject, useCallback, useMemo, useState } from "react";
2
+ import { CommandStatus } from "@lookiero/messaging-react";
3
+ import { PaymentFlowRef } from "@lookiero/payments-front";
4
+ import { Logger } from "@lookiero/sty-psp-logging";
5
+ import { NotificationLevel, useCreateToastNotification } from "@lookiero/sty-psp-notifications";
6
+ import {
7
+ ViewCheckoutBookingById,
8
+ viewCheckoutBookingById,
9
+ ViewCheckoutBookingByIdResult,
10
+ } from "../../../projection/checkoutBooking/viewCheckoutBookingById";
11
+ import { PaymentFlowPayloadProjection } from "../../../projection/payment/paymentFlowPayload";
12
+ import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
13
+ import { useSubmitCheckout as useSubmitCheckoutCommand } from "../../domain/checkout/react/useSubmitCheckout";
14
+ import { useBlockCheckoutBooking } from "../../domain/checkoutBooking/react/useBlockCheckoutBooking";
15
+ import { I18nMessages } from "../i18n/i18n";
16
+ import { useQueryBus } from "./useQueryBus";
17
+
18
+ enum ChargeStatus {
19
+ EXECUTED = "EXECUTED",
20
+ REQUIRES_ACTION = "REQUIRES_ACTION",
21
+ REQUIRED_ACTION_CANCELLED = "REQUIRED_ACTION_CANCELLED",
22
+ REJECTED = "REJECTED",
23
+ CANCELLED = "CANCELLED",
24
+ TO_CONFIRM = "TO_CONFIRM",
25
+ ERROR = "ERROR",
26
+ UNKNOWN = "UNKNOWN",
27
+ }
28
+ interface LegacyBoxCheckout {
29
+ readonly status: ChargeStatus;
30
+ readonly toaster: {
31
+ id: string;
32
+ } | null;
33
+ readonly final: boolean;
34
+ readonly id?: string;
35
+ readonly translation?: string;
36
+ }
37
+
38
+ type Status = "idle" | "loading" | "success" | "error";
39
+
40
+ interface SubmitCheckoutFunctionArgs {
41
+ readonly paymentFlowPayload: PaymentFlowPayloadProjection;
42
+ readonly sizeChangeEnabled: boolean;
43
+ }
44
+
45
+ interface SubmitCheckoutFunction {
46
+ (args: SubmitCheckoutFunctionArgs): Promise<void>;
47
+ }
48
+
49
+ type UseSubmitCheckoutResult = [submitCheckout: SubmitCheckoutFunction, status: Status];
50
+
51
+ interface UseSubmitCheckoutFunctionArgs {
52
+ readonly checkoutId: string;
53
+ readonly checkoutBookingId: string;
54
+ readonly paymentFlowRef: RefObject<PaymentFlowRef>;
55
+ readonly onError: () => void;
56
+ readonly onSuccess?: () => void;
57
+ readonly logger: Logger;
58
+ }
59
+
60
+ interface UseSubmitCheckoutFunction {
61
+ (args: UseSubmitCheckoutFunctionArgs): UseSubmitCheckoutResult;
62
+ }
63
+
64
+ const useSubmitCheckout: UseSubmitCheckoutFunction = ({
65
+ checkoutId,
66
+ checkoutBookingId,
67
+ paymentFlowRef,
68
+ onError,
69
+ onSuccess,
70
+ logger,
71
+ }) => {
72
+ const queryBus = useQueryBus();
73
+ const [submitCheckoutCommand, submitCheckoutCommandStatus] = useSubmitCheckoutCommand({ checkoutId, logger });
74
+ const [blockCheckoutBooking, blockCheckoutBookingStatus] = useBlockCheckoutBooking({ checkoutBookingId, logger });
75
+ const [createNotification] = useCreateToastNotification({ contextId: MESSAGING_CONTEXT_ID, logger });
76
+ const [checkoutBookingExpired, setCheckoutBookingExpired] = useState(false);
77
+
78
+ const [startLegacyBoxCheckoutStatus, setStartLegacyBoxCheckoutStatus] = useState<Status>("idle");
79
+
80
+ const submitCheckout: SubmitCheckoutFunction = useCallback(
81
+ async ({ paymentFlowPayload, sizeChangeEnabled }) => {
82
+ try {
83
+ sizeChangeEnabled && (await blockCheckoutBooking());
84
+ } catch (error) {
85
+ return;
86
+ }
87
+
88
+ const checkoutBooking = await queryBus<ViewCheckoutBookingById, ViewCheckoutBookingByIdResult>(
89
+ viewCheckoutBookingById({ checkoutBookingId }),
90
+ );
91
+
92
+ if (checkoutBooking?.isExpired) {
93
+ setCheckoutBookingExpired(true);
94
+ return;
95
+ }
96
+
97
+ paymentFlowRef.current?.startLegacyBoxCheckout(
98
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
99
+ // @ts-ignore
100
+ paymentFlowPayload,
101
+ async ({ status, toaster, final }: LegacyBoxCheckout) => {
102
+ setStartLegacyBoxCheckoutStatus("loading");
103
+
104
+ if (final) {
105
+ if (status === ChargeStatus.EXECUTED) {
106
+ setStartLegacyBoxCheckoutStatus("success");
107
+ await submitCheckoutCommand();
108
+ onSuccess?.();
109
+ } else {
110
+ createNotification({
111
+ level: NotificationLevel.ERROR,
112
+ bodyI18nKey: toaster?.id || I18nMessages.CHECKOUT_TOAST_PAYMENT_ERROR,
113
+ });
114
+ setStartLegacyBoxCheckoutStatus("error");
115
+ }
116
+ }
117
+ },
118
+ );
119
+ },
120
+ [
121
+ queryBus,
122
+ checkoutBookingId,
123
+ paymentFlowRef,
124
+ blockCheckoutBooking,
125
+ submitCheckoutCommand,
126
+ onSuccess,
127
+ createNotification,
128
+ ],
129
+ );
130
+
131
+ const status: Status = useMemo(() => {
132
+ if (
133
+ blockCheckoutBookingStatus === CommandStatus.LOADING ||
134
+ startLegacyBoxCheckoutStatus === "loading" ||
135
+ submitCheckoutCommandStatus === CommandStatus.LOADING
136
+ ) {
137
+ return "loading";
138
+ }
139
+ if (
140
+ blockCheckoutBookingStatus === CommandStatus.SUCCESS &&
141
+ startLegacyBoxCheckoutStatus === "success" &&
142
+ submitCheckoutCommandStatus === CommandStatus.SUCCESS
143
+ ) {
144
+ return "success";
145
+ }
146
+ if (
147
+ blockCheckoutBookingStatus === CommandStatus.ERROR ||
148
+ startLegacyBoxCheckoutStatus === "error" ||
149
+ submitCheckoutCommandStatus === CommandStatus.ERROR ||
150
+ checkoutBookingExpired
151
+ ) {
152
+ onError();
153
+ return "error";
154
+ }
155
+
156
+ return "idle";
157
+ }, [
158
+ blockCheckoutBookingStatus,
159
+ startLegacyBoxCheckoutStatus,
160
+ submitCheckoutCommandStatus,
161
+ checkoutBookingExpired,
162
+ onError,
163
+ ]);
164
+
165
+ return [submitCheckout, status];
166
+ };
167
+
168
+ export type { Status, SubmitCheckoutFunction };
169
+ export { useSubmitCheckout };
@@ -50,7 +50,6 @@ 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",
54
53
  CHECKOUT_SUCCESS_MODAL_TITLE = "checkout.success_modal_title",
55
54
  CHECKOUT_SUCCESS_MODAL_DESCRIPTION = "checkout.success_modal_description",
56
55
  CHECKOUT_SUCCESS_MODAL_BUTTON = "checkout.success_modal_button",
@@ -7,8 +7,11 @@ import { generatePath } from "react-router-native";
7
7
  import { createMemoryRouter, RouterProvider } from "react-router-native";
8
8
  import { CommandStatus, QueryStatus } from "@lookiero/messaging-react";
9
9
  import { Logger } from "@lookiero/sty-psp-logging";
10
+ import { Tradename } from "@lookiero/sty-sp-tradename";
10
11
  import { CheckoutStatus } from "../../../domain/checkout/model/checkout";
11
12
  import { CheckoutItemStatus } from "../../../domain/checkoutItem/model/checkoutItem";
13
+ import { Customer } from "../../../projection/customer/customer";
14
+ import { KameleoonEnvironment } from "../../ab-testing/kameleoonEnvironment";
12
15
  import { useStartCheckout } from "../../domain/checkout/react/useStartCheckout";
13
16
  import { checkout } from "../../projection/checkout/checkout.mock";
14
17
  import { useViewFirstAvailableCheckoutByCustomerId } from "../../projection/checkout/react/useViewFirstAvailableCheckoutByCustomerId";
@@ -91,7 +94,12 @@ const renderCheckoutMiddleware = ({
91
94
  {
92
95
  path: "/*",
93
96
  element: (
94
- <StaticInfoProvider basePath="" customer={} kameleoon={}>
97
+ <StaticInfoProvider
98
+ basePath=""
99
+ customer={{} as Customer}
100
+ kameleoon={{} as KameleoonEnvironment}
101
+ tradename={Tradename.LOOKIERO}
102
+ >
95
103
  <CheckoutMiddleware
96
104
  customerId={customerId}
97
105
  loader={<Text>{loaderText}</Text>}
@@ -48,6 +48,12 @@ 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));
51
57
 
52
58
  const [checkout] = useViewFirstAvailableCheckoutByCustomerId({ customerId });
53
59
  const checkoutItemsRef = useRef<CheckoutItemProjection[]>();
@@ -93,7 +99,8 @@ const CheckoutMiddleware: FC<CheckoutMiddlewareProps> = ({
93
99
  summaryRouteMatchRef.current ||
94
100
  summaryTabsRouteMatchRef.current ||
95
101
  itemDetailRouteMatchRef.current ||
96
- checkoutRouteMatchRef.current
102
+ checkoutRouteMatchRef.current ||
103
+ checkoutPaymentRouteMatchRef.current
97
104
  )
98
105
  ) {
99
106
  navigateRef.current(`${basePath}/${Routes.SUMMARY}`, { replace: true });
@@ -120,6 +127,13 @@ const CheckoutMiddleware: FC<CheckoutMiddlewareProps> = ({
120
127
  return null;
121
128
  }
122
129
 
130
+ /* Prevent direct payment access */
131
+ if (checkoutPaymentRouteMatch && !checkoutShown.current) {
132
+ onNotAccessible();
133
+
134
+ return null;
135
+ }
136
+
123
137
  return children;
124
138
  };
125
139
 
@@ -5,13 +5,15 @@ import { I18n } from "@lookiero/i18n-react";
5
5
  import { Kameleoon } from "@lookiero/sty-psp-ab-testing";
6
6
  import { Locale } from "@lookiero/sty-psp-locale";
7
7
  import { Layout } from "@lookiero/sty-psp-ui";
8
+ import { Tradename } from "@lookiero/sty-sp-tradename";
8
9
  import { Customer } from "../../../projection/customer/customer";
9
- import { OrderProjection } from "../../../projection/order/order";
10
- import { SubscriptionProjection } from "../../../projection/subscription/subscription";
10
+ import { Order } from "../../../projection/order/order";
11
+ import { Subscription } from "../../../projection/subscription/subscription";
11
12
  import { KameleoonEnvironment } from "../../ab-testing/kameleoonEnvironment";
12
13
  import { StaticInfoProvider } from "../hooks/useStaticInfo";
13
14
  import { App } from "../views/App";
14
15
  import { Checkout } from "../views/checkout/Checkout";
16
+ import { CheckoutPaymentModal } from "../views/checkout/components/checkoutPaymentModal/CheckoutPaymentModal";
15
17
  import { Feedback } from "../views/feedback/Feedback";
16
18
  import { Item } from "../views/item/Item";
17
19
  import { Return } from "../views/return/Return";
@@ -23,15 +25,16 @@ import { Routes } from "./routes";
23
25
  interface RoutingProps {
24
26
  readonly basePath?: string;
25
27
  readonly customer: Customer;
26
- readonly order: OrderProjection;
27
- readonly subscription: SubscriptionProjection;
28
+ readonly order: Order | undefined;
29
+ readonly subscription: Subscription | undefined;
28
30
  readonly locale: Locale;
29
31
  readonly I18n: I18n;
30
32
  readonly kameleoon: KameleoonEnvironment;
31
33
  readonly layout: Layout;
34
+ readonly tradename: Tradename;
32
35
  readonly getAuthToken: () => Promise<string>;
33
36
  readonly onNotAccessible: () => void;
34
- readonly onCheckoutFlowSuccess: () => void;
37
+ readonly onCheckoutSubmitted?: () => void;
35
38
  readonly onI18nError?: (err: Error) => void;
36
39
  readonly useRedirect: () => Record<string, string>;
37
40
  readonly useRoutes: typeof reactRouterUseRoutes;
@@ -46,10 +49,11 @@ const Routing: FC<RoutingProps> = ({
46
49
  I18n,
47
50
  kameleoon,
48
51
  layout,
52
+ tradename,
49
53
  getAuthToken,
50
54
  onI18nError,
51
55
  onNotAccessible,
52
- onCheckoutFlowSuccess,
56
+ onCheckoutSubmitted,
53
57
  useRedirect,
54
58
  useRoutes = reactRouterUseRoutes,
55
59
  }) => {
@@ -57,7 +61,7 @@ const Routing: FC<RoutingProps> = ({
57
61
  {
58
62
  path: "",
59
63
  element: (
60
- <StaticInfoProvider basePath={basePath} customer={customer} kameleoon={kameleoon}>
64
+ <StaticInfoProvider basePath={basePath} customer={customer} kameleoon={kameleoon} tradename={tradename}>
61
65
  <I18n loader={<Spinner />} locale={locale} onError={onI18nError}>
62
66
  <Kameleoon loader={<Spinner />} siteCode={kameleoon.siteCode}>
63
67
  <CheckoutMiddleware customerId={customer?.customerId as string} onNotAccessible={onNotAccessible}>
@@ -110,16 +114,26 @@ const Routing: FC<RoutingProps> = ({
110
114
  path: Routes.CHECKOUT,
111
115
  element: (
112
116
  <Suspense fallback={<Spinner />}>
113
- <Checkout
114
- getAuthToken={getAuthToken}
115
- layout={layout}
116
- order={order}
117
- subscription={subscription}
118
- useRedirect={useRedirect}
119
- onCheckoutFlowSuccess={onCheckoutFlowSuccess}
120
- />
117
+ <Checkout layout={layout} useRedirect={useRedirect}>
118
+ <Outlet />
119
+ </Checkout>
121
120
  </Suspense>
122
121
  ),
122
+ children: [
123
+ {
124
+ path: Routes.CHECKOUT_PAYMENT,
125
+ element: (
126
+ <CheckoutPaymentModal
127
+ coupon={order?.coupon || null}
128
+ getAuthToken={getAuthToken}
129
+ isFirstOrder={order?.isFirstOrder as boolean}
130
+ orderNumber={order?.orderNumber as number}
131
+ subscription={subscription as Subscription}
132
+ onCheckoutSubmitted={onCheckoutSubmitted}
133
+ />
134
+ ),
135
+ },
136
+ ],
123
137
  },
124
138
  {
125
139
  path: Routes.FEEDBACK,
@@ -5,6 +5,7 @@ export enum Routes {
5
5
  SUMMARY = "summary",
6
6
  SUMMARY_TABS = "tabs/:tab",
7
7
  CHECKOUT = "checkout",
8
+ CHECKOUT_PAYMENT = "payment",
8
9
  FEEDBACK = "feedback",
9
10
  RETURN = "return/:id",
10
11
  }
@@ -1,7 +1,8 @@
1
- import { PortalHost } from "@gorhom/portal";
1
+ import { PortalProvider } from "@gorhom/portal";
2
2
  import React, { FC } from "react";
3
3
  import { StatusBar } from "react-native";
4
4
  import { SafeAreaProvider } from "react-native-safe-area-context";
5
+ import { PortalProvider as AuroraPortalProvider } from "@lookiero/aurora";
5
6
  import { Notifications } from "@lookiero/sty-psp-notifications";
6
7
  import { theme } from "@lookiero/sty-psp-ui";
7
8
  import { MESSAGING_CONTEXT_ID } from "../../delivery/baseBootstrap";
@@ -15,10 +16,17 @@ interface AppProps {
15
16
 
16
17
  const App: FC<AppProps> = ({ children }) => (
17
18
  <SafeAreaProvider>
18
- <StatusBar backgroundColor={colorBgBase} barStyle="dark-content" translucent />
19
- <Notifications contextId={MESSAGING_CONTEXT_ID} domain={DOMAIN} portalHostName="Checkout" />
20
- <PortalHost name="Checkout" />
21
- {children}
19
+ <PortalProvider rootHostName="Checkout">
20
+ <StatusBar backgroundColor={colorBgBase} barStyle="dark-content" translucent />
21
+ <Notifications contextId={MESSAGING_CONTEXT_ID} domain={DOMAIN} portalHostName="Checkout" />
22
+ {/*
23
+ We are using the Aurora's PortalProvider at this level for notifications to work properly.
24
+
25
+ PaymentInstrumentSelect uses Aurora's Portal, and if we rely on UAF's Portal (injected by <Aurora>)
26
+ notifications would be displayed in a layer below Portal's one (not visible).
27
+ */}
28
+ <AuroraPortalProvider>{children}</AuroraPortalProvider>
29
+ </PortalProvider>
22
30
  </SafeAreaProvider>
23
31
  );
24
32