richie-education 2.25.0-b2.dev78 → 2.25.0-b2.dev80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/joanie.ts +1 -1
- package/js/components/PaymentButton/index.spec.tsx +153 -22
- package/js/components/PaymentButton/index.tsx +8 -15
- package/js/components/SaleTunnel/components/SaleTunnelStepResume/index.spec.tsx +0 -3
- package/js/components/SaleTunnel/context.tsx +0 -1
- package/js/components/SaleTunnel/index.tsx +9 -9
- package/js/hooks/useOrders.ts +6 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +6 -6
- package/package.json +1 -1
package/js/api/joanie.ts
CHANGED
|
@@ -306,7 +306,7 @@ const API = (): Joanie.API => {
|
|
|
306
306
|
},
|
|
307
307
|
orders: {
|
|
308
308
|
abort: async ({ id, payment_id }) => {
|
|
309
|
-
fetchWithJWT(ROUTES.user.orders.abort.replace(':id', id), {
|
|
309
|
+
return fetchWithJWT(ROUTES.user.orders.abort.replace(':id', id), {
|
|
310
310
|
method: 'POST',
|
|
311
311
|
body: payment_id ? JSON.stringify({ payment_id }) : undefined,
|
|
312
312
|
}).then(checkStatus);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
|
-
import { PropsWithChildren, useMemo
|
|
3
|
+
import { PropsWithChildren, useMemo } from 'react';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
6
6
|
import { faker } from '@faker-js/faker';
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
OrderCredentialCreationPayload,
|
|
26
26
|
OrderState,
|
|
27
27
|
ProductType,
|
|
28
|
-
Order,
|
|
29
28
|
OrderGroup,
|
|
30
29
|
CertificateProduct,
|
|
31
30
|
CredentialProduct,
|
|
@@ -42,6 +41,7 @@ import {
|
|
|
42
41
|
SaleTunnelCertificateContext,
|
|
43
42
|
} from 'components/SaleTunnel/context';
|
|
44
43
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
44
|
+
import useProductOrder from 'hooks/useProductOrder';
|
|
45
45
|
import PaymentButton from '.';
|
|
46
46
|
|
|
47
47
|
jest.mock('utils/context', () => ({
|
|
@@ -62,6 +62,12 @@ jest.mock('utils/context', () => ({
|
|
|
62
62
|
|
|
63
63
|
jest.mock('./components/PaymentInterfaces');
|
|
64
64
|
|
|
65
|
+
type WrapperProps = PropsWithChildren<{
|
|
66
|
+
client?: QueryClient;
|
|
67
|
+
product: CredentialProduct | CertificateProduct;
|
|
68
|
+
orderGroup?: OrderGroup;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
65
71
|
describe.each([
|
|
66
72
|
{
|
|
67
73
|
productType: ProductType.CREDENTIAL,
|
|
@@ -88,24 +94,22 @@ describe.each([
|
|
|
88
94
|
style: 'currency',
|
|
89
95
|
}).format(price);
|
|
90
96
|
|
|
91
|
-
const
|
|
92
|
-
client = createTestQueryClient({ user: true }),
|
|
93
|
-
children,
|
|
97
|
+
const SaleTunnelWrapper = ({
|
|
94
98
|
product,
|
|
95
99
|
orderGroup,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
children,
|
|
101
|
+
}: Exclude<WrapperProps, 'client'>) => {
|
|
102
|
+
const { item: order } = useProductOrder({
|
|
103
|
+
courseCode: product.type === ProductType.CREDENTIAL ? TEST_COURSE_CODE : undefined,
|
|
104
|
+
enrollmentId: product.type === ProductType.CERTIFICATE ? TEST_ENROLLMENT_ID : undefined,
|
|
105
|
+
productId: product.id,
|
|
106
|
+
});
|
|
102
107
|
|
|
103
108
|
const context: SaleTunnelContextType = useMemo(() => {
|
|
104
109
|
if (product.type === ProductType.CREDENTIAL) {
|
|
105
110
|
return {
|
|
106
111
|
product,
|
|
107
112
|
order,
|
|
108
|
-
setOrder,
|
|
109
113
|
key: `${TEST_COURSE_CODE}+${product.id}`,
|
|
110
114
|
course: CourseLightFactory({ code: TEST_COURSE_CODE }).one(),
|
|
111
115
|
orderGroup,
|
|
@@ -114,19 +118,25 @@ describe.each([
|
|
|
114
118
|
return {
|
|
115
119
|
product,
|
|
116
120
|
order,
|
|
117
|
-
setOrder,
|
|
118
121
|
key: `${TEST_ENROLLMENT_ID}+${product.id}`,
|
|
119
122
|
enrollment: EnrollmentFactory({ id: TEST_ENROLLMENT_ID }).one(),
|
|
120
123
|
orderGroup,
|
|
121
124
|
} as SaleTunnelCertificateContext;
|
|
122
125
|
}
|
|
123
|
-
}, [product, order,
|
|
126
|
+
}, [product, order, orderGroup]);
|
|
127
|
+
|
|
128
|
+
return <SaleTunnelContext.Provider value={context}>{children}</SaleTunnelContext.Provider>;
|
|
129
|
+
};
|
|
124
130
|
|
|
131
|
+
const Wrapper = ({
|
|
132
|
+
client = createTestQueryClient({ user: true }),
|
|
133
|
+
...props
|
|
134
|
+
}: WrapperProps) => {
|
|
125
135
|
return (
|
|
126
136
|
<IntlProvider locale="en">
|
|
127
137
|
<QueryClientProvider client={client}>
|
|
128
138
|
<JoanieSessionProvider>
|
|
129
|
-
<
|
|
139
|
+
<SaleTunnelWrapper {...props} />
|
|
130
140
|
</JoanieSessionProvider>
|
|
131
141
|
</QueryClientProvider>
|
|
132
142
|
</IntlProvider>
|
|
@@ -332,7 +342,7 @@ describe.each([
|
|
|
332
342
|
<PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
|
|
333
343
|
</Wrapper>,
|
|
334
344
|
);
|
|
335
|
-
nbApiCalls += 1; //
|
|
345
|
+
nbApiCalls += 1; // useProductOrder call.
|
|
336
346
|
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
337
347
|
|
|
338
348
|
const $terms = screen.getByLabelText(
|
|
@@ -356,8 +366,7 @@ describe.each([
|
|
|
356
366
|
|
|
357
367
|
// - Route to create order should have been called
|
|
358
368
|
nbApiCalls += 1; // order post create (invalidate queries)
|
|
359
|
-
nbApiCalls += 1; //
|
|
360
|
-
nbApiCalls += 1; // refetch useProductOrder
|
|
369
|
+
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
361
370
|
nbApiCalls += 1; // order submit
|
|
362
371
|
|
|
363
372
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
@@ -410,6 +419,126 @@ describe.each([
|
|
|
410
419
|
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
411
420
|
});
|
|
412
421
|
|
|
422
|
+
it('should create an order only the first time the payment interface is shown, and not after aborting', async () => {
|
|
423
|
+
const product: Joanie.Product = ProductFactory().one();
|
|
424
|
+
const billingAddress: Joanie.Address = AddressFactory().one();
|
|
425
|
+
const handleSuccess = jest.fn();
|
|
426
|
+
const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
|
|
427
|
+
|
|
428
|
+
const fetchOrderQueryParams =
|
|
429
|
+
product.type === ProductType.CREDENTIAL
|
|
430
|
+
? {
|
|
431
|
+
course_code: TEST_COURSE_CODE,
|
|
432
|
+
product_id: product.id,
|
|
433
|
+
state: ['pending', 'validated', 'submitted'],
|
|
434
|
+
}
|
|
435
|
+
: {
|
|
436
|
+
enrollment_id: TEST_ENROLLMENT_ID,
|
|
437
|
+
product_id: product.id,
|
|
438
|
+
state: ['pending', 'validated', 'submitted'],
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
fetchMock
|
|
442
|
+
.get(
|
|
443
|
+
`https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
|
|
444
|
+
[],
|
|
445
|
+
)
|
|
446
|
+
.post('https://joanie.test/api/v1.0/orders/', order)
|
|
447
|
+
.patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
|
|
448
|
+
paymentInfo,
|
|
449
|
+
})
|
|
450
|
+
.get(`https://joanie.test/api/v1.0/orders/${order.id}/`, {
|
|
451
|
+
...order,
|
|
452
|
+
})
|
|
453
|
+
.post(`https://joanie.test/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK);
|
|
454
|
+
|
|
455
|
+
render(
|
|
456
|
+
<Wrapper client={createTestQueryClient({ user: true })} product={product}>
|
|
457
|
+
<PaymentButton billingAddress={billingAddress} onSuccess={handleSuccess} />
|
|
458
|
+
</Wrapper>,
|
|
459
|
+
);
|
|
460
|
+
nbApiCalls += 1; // useProductOrder call.
|
|
461
|
+
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
462
|
+
|
|
463
|
+
const $terms = screen.getByLabelText(
|
|
464
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
465
|
+
);
|
|
466
|
+
await act(async () => {
|
|
467
|
+
fireEvent.click($terms);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const $button = screen.getByRole('button', {
|
|
471
|
+
name: `Pay ${formatPrice(product.price, product.price_currency)}`,
|
|
472
|
+
}) as HTMLButtonElement;
|
|
473
|
+
|
|
474
|
+
// - Payment button should not be disabled.
|
|
475
|
+
expect($button.disabled).toBe(false);
|
|
476
|
+
|
|
477
|
+
// - User clicks on pay button
|
|
478
|
+
await act(async () => {
|
|
479
|
+
fireEvent.click($button);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// - Route to create order should have been called
|
|
483
|
+
nbApiCalls += 1; // order post create (invalidate queries)
|
|
484
|
+
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
485
|
+
nbApiCalls += 1; // order submit
|
|
486
|
+
|
|
487
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
488
|
+
expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
|
|
489
|
+
|
|
490
|
+
// - Spinner should be displayed
|
|
491
|
+
screen.getByText('Payment in progress');
|
|
492
|
+
|
|
493
|
+
// - Payment interface should be displayed
|
|
494
|
+
screen.getByText('Payment interface component');
|
|
495
|
+
|
|
496
|
+
// - Simulate the payment aborting.
|
|
497
|
+
fetchMock.get(
|
|
498
|
+
`https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
|
|
499
|
+
[
|
|
500
|
+
{
|
|
501
|
+
...order,
|
|
502
|
+
state: OrderState.PENDING,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
{ overwriteRoutes: true },
|
|
506
|
+
);
|
|
507
|
+
await act(async () => {
|
|
508
|
+
fireEvent.click(screen.getByTestId('payment-abort'));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
nbApiCalls += 1; // abort order.
|
|
512
|
+
nbApiCalls += 1; // useProductOrder call (invalidate from create)
|
|
513
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
514
|
+
expect(fetchMock.calls()[fetchMock.calls().length - 2][0]).toBe(
|
|
515
|
+
`https://joanie.test/api/v1.0/orders/${order.id}/abort/`,
|
|
516
|
+
);
|
|
517
|
+
expect(fetchMock.calls()[fetchMock.calls().length - 1][0]).toBe(
|
|
518
|
+
`https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
screen.getByText('You have aborted the payment.');
|
|
522
|
+
|
|
523
|
+
// screen.logTestingPlaygroundURL();
|
|
524
|
+
|
|
525
|
+
// - User clicks on pay button again.
|
|
526
|
+
await act(async () => {
|
|
527
|
+
fireEvent.click($button);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// - Spinner should be displayed
|
|
531
|
+
screen.getByText('Payment in progress');
|
|
532
|
+
|
|
533
|
+
// - Payment interface should be displayed
|
|
534
|
+
screen.getByText('Payment interface component');
|
|
535
|
+
|
|
536
|
+
// - Now we make sure the order is not created again and just submitted.
|
|
537
|
+
nbApiCalls += 1; // submits order.
|
|
538
|
+
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
539
|
+
expect(fetchMock.lastUrl()).toBe(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`);
|
|
540
|
+
});
|
|
541
|
+
|
|
413
542
|
it('should render a payment button and not call the order creation route', async () => {
|
|
414
543
|
const product: Joanie.Product = ProductFactory().one();
|
|
415
544
|
const billingAddress: Joanie.Address = AddressFactory().one();
|
|
@@ -655,8 +784,9 @@ describe.each([
|
|
|
655
784
|
|
|
656
785
|
await waitFor(
|
|
657
786
|
async () => {
|
|
658
|
-
|
|
659
|
-
expect(fetchMock.
|
|
787
|
+
// +1 is for useProductOrder call invalidation.
|
|
788
|
+
expect(fetchMock.calls()).toHaveLength(PAYMENT_SETTINGS.pollLimit + 1);
|
|
789
|
+
expect(fetchMock.calls()[fetchMock.calls().length - 2][0]).toBe(
|
|
660
790
|
`https://joanie.test/api/v1.0/orders/${order.id}/abort/`,
|
|
661
791
|
);
|
|
662
792
|
},
|
|
@@ -665,7 +795,9 @@ describe.each([
|
|
|
665
795
|
},
|
|
666
796
|
);
|
|
667
797
|
|
|
668
|
-
expect(
|
|
798
|
+
expect(
|
|
799
|
+
JSON.parse(fetchMock.calls()[fetchMock.calls().length - 2][1]!.body!.toString()),
|
|
800
|
+
).toEqual({
|
|
669
801
|
payment_id: paymentInfo.payment_id,
|
|
670
802
|
});
|
|
671
803
|
|
|
@@ -735,7 +867,6 @@ describe.each([
|
|
|
735
867
|
|
|
736
868
|
// - Route to create order should have been called
|
|
737
869
|
nbApiCalls += 1; // order post create (invalidate queries)
|
|
738
|
-
nbApiCalls += 1; // refetch omniscient orders
|
|
739
870
|
nbApiCalls += 1; // refetch useProductOrder
|
|
740
871
|
nbApiCalls += 1; // order submit
|
|
741
872
|
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
@@ -3,7 +3,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
|
3
3
|
import { Button } from '@openfun/cunningham-react';
|
|
4
4
|
import { Spinner } from 'components/Spinner';
|
|
5
5
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
6
|
-
import { useOmniscientOrders } from 'hooks/useOrders';
|
|
7
6
|
import { PAYMENT_SETTINGS } from 'settings';
|
|
8
7
|
import type * as Joanie from 'types/Joanie';
|
|
9
8
|
import { OrderCreationPayload, OrderState, ProductType } from 'types/Joanie';
|
|
@@ -11,10 +10,10 @@ import type { Nullable } from 'types/utils';
|
|
|
11
10
|
import { HttpError } from 'utils/errors/HttpError';
|
|
12
11
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
13
12
|
import { CourseProductEvent } from 'types/web-analytics';
|
|
14
|
-
import useProductOrder from 'hooks/useProductOrder';
|
|
15
13
|
import { useTerms } from 'components/PaymentButton/hooks/useTerms';
|
|
16
14
|
import { useSaleTunnelContext } from 'components/SaleTunnel/context';
|
|
17
15
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
16
|
+
import { useOrders } from 'hooks/useOrders';
|
|
18
17
|
import PaymentInterface from './components/PaymentInterfaces';
|
|
19
18
|
|
|
20
19
|
const messages = defineMessages({
|
|
@@ -99,13 +98,8 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
99
98
|
const intl = useIntl();
|
|
100
99
|
const API = useJoanieApi();
|
|
101
100
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
102
|
-
const { course, key, enrollment, product,
|
|
103
|
-
const {
|
|
104
|
-
courseCode: course?.code,
|
|
105
|
-
enrollmentId: enrollment?.id,
|
|
106
|
-
productId: product.id,
|
|
107
|
-
});
|
|
108
|
-
const orderManager = useOmniscientOrders();
|
|
101
|
+
const { course, key, enrollment, product, order, orderGroup } = useSaleTunnelContext();
|
|
102
|
+
const { methods: orderMethods } = useOrders(undefined, { enabled: false });
|
|
109
103
|
const [payment, setPayment] = useState<PaymentInfo | OneClickPaymentInfo>();
|
|
110
104
|
const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
|
|
111
105
|
const [error, setError] = useState<PaymentErrorMessageId>(PaymentErrorMessageId.ERROR_DEFAULT);
|
|
@@ -156,7 +150,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
156
150
|
if (!paymentInfos) {
|
|
157
151
|
const billingAddressPayload = ObjectHelper.omit(billingAddress!, 'id', 'is_main');
|
|
158
152
|
|
|
159
|
-
|
|
153
|
+
orderMethods.submit(
|
|
160
154
|
{
|
|
161
155
|
id: orderId,
|
|
162
156
|
billing_address: billingAddressPayload,
|
|
@@ -214,9 +208,8 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
214
208
|
...(orderGroup ? { order_group_id: orderGroup.id } : {}),
|
|
215
209
|
};
|
|
216
210
|
|
|
217
|
-
|
|
211
|
+
orderMethods.create(payload, {
|
|
218
212
|
onSuccess: (newOrder) => {
|
|
219
|
-
setOrder(newOrder);
|
|
220
213
|
createPayment(newOrder.id);
|
|
221
214
|
},
|
|
222
215
|
onError: async () => {
|
|
@@ -232,7 +225,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
232
225
|
const checkOrderValidity = async () => {
|
|
233
226
|
if (round >= PAYMENT_SETTINGS.pollLimit) {
|
|
234
227
|
timeoutRef.current = undefined;
|
|
235
|
-
|
|
228
|
+
orderMethods.abort({ id: payment!.order_id, payment_id: payment!.payment_id });
|
|
236
229
|
setState(ComponentStates.ERROR);
|
|
237
230
|
} else {
|
|
238
231
|
const isValidated = await isOrderValidated(payment!.order_id);
|
|
@@ -260,7 +253,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
260
253
|
if (timeoutRef.current !== undefined) {
|
|
261
254
|
clearTimeout(timeoutRef.current);
|
|
262
255
|
if (payment) {
|
|
263
|
-
|
|
256
|
+
orderMethods.abort({
|
|
264
257
|
id: payment.order_id,
|
|
265
258
|
payment_id: payment.payment_id,
|
|
266
259
|
});
|
|
@@ -271,7 +264,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
|
|
|
271
264
|
|
|
272
265
|
useEffect(() => {
|
|
273
266
|
if (error === PaymentErrorMessageId.ERROR_ABORTING) {
|
|
274
|
-
|
|
267
|
+
orderMethods
|
|
275
268
|
.abort({
|
|
276
269
|
id: payment!.order_id,
|
|
277
270
|
payment_id: payment!.payment_id,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
ProductFactory,
|
|
7
7
|
} from 'utils/test/factories/joanie';
|
|
8
8
|
import { SaleTunnelContext } from 'components/SaleTunnel/context';
|
|
9
|
-
import { noop } from 'utils';
|
|
10
9
|
import { SaleTunnelStepResume } from '.';
|
|
11
10
|
|
|
12
11
|
describe('SaleTunnelStepResume', () => {
|
|
@@ -24,7 +23,6 @@ describe('SaleTunnelStepResume', () => {
|
|
|
24
23
|
<SaleTunnelContext.Provider
|
|
25
24
|
value={{
|
|
26
25
|
product,
|
|
27
|
-
setOrder: noop,
|
|
28
26
|
course: CourseLightFactory({ code: '00000' }).one(),
|
|
29
27
|
key: `00000+${product.id}`,
|
|
30
28
|
}}
|
|
@@ -56,7 +54,6 @@ describe('SaleTunnelStepResume', () => {
|
|
|
56
54
|
value={{
|
|
57
55
|
product,
|
|
58
56
|
order,
|
|
59
|
-
setOrder: noop,
|
|
60
57
|
course: CourseLightFactory({ code: '00000' }).one(),
|
|
61
58
|
key: `00000+${product.id}`,
|
|
62
59
|
}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { defineMessages, useIntl } from 'react-intl';
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { Modal } from 'components/Modal';
|
|
@@ -16,7 +16,7 @@ import { IconTypeEnum } from 'components/Icon';
|
|
|
16
16
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
17
17
|
import { CourseProductEvent } from 'types/web-analytics';
|
|
18
18
|
import { Manifest, useStepManager } from 'hooks/useStepManager';
|
|
19
|
-
import
|
|
19
|
+
import useProductOrder from 'hooks/useProductOrder';
|
|
20
20
|
import { SaleTunnelContext, SaleTunnelContextType } from './context';
|
|
21
21
|
import { StepBreadcrumb } from './components/StepBreadcrumb';
|
|
22
22
|
import { SaleTunnelStepValidation } from './components/SaleTunnelStepValidation';
|
|
@@ -80,8 +80,11 @@ const SaleTunnel = ({
|
|
|
80
80
|
product.id
|
|
81
81
|
}`;
|
|
82
82
|
const queryClient = useQueryClient();
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const { item: order } = useProductOrder({
|
|
84
|
+
courseCode: course?.code,
|
|
85
|
+
enrollmentId: enrollment?.id,
|
|
86
|
+
productId: product.id,
|
|
87
|
+
});
|
|
85
88
|
|
|
86
89
|
const manifest: Manifest<TunnelSteps, 'resume'> = {
|
|
87
90
|
start: 'validation',
|
|
@@ -138,7 +141,6 @@ const SaleTunnel = ({
|
|
|
138
141
|
return {
|
|
139
142
|
product: product as CredentialProduct,
|
|
140
143
|
order,
|
|
141
|
-
setOrder,
|
|
142
144
|
key,
|
|
143
145
|
course: course!,
|
|
144
146
|
enrollment: undefined,
|
|
@@ -147,25 +149,23 @@ const SaleTunnel = ({
|
|
|
147
149
|
return {
|
|
148
150
|
product: product as CertificateProduct,
|
|
149
151
|
order,
|
|
150
|
-
setOrder,
|
|
151
152
|
key,
|
|
152
153
|
course: undefined,
|
|
153
154
|
enrollment: enrollment!,
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
|
-
}, [product, order
|
|
157
|
+
}, [product, order]);
|
|
157
158
|
|
|
158
159
|
useMemo(
|
|
159
160
|
() => ({
|
|
160
161
|
product,
|
|
161
162
|
order,
|
|
162
|
-
setOrder,
|
|
163
163
|
key,
|
|
164
164
|
course,
|
|
165
165
|
enrollment,
|
|
166
166
|
orderGroup,
|
|
167
167
|
}),
|
|
168
|
-
[product, order,
|
|
168
|
+
[product, order, key, course, enrollment, orderGroup],
|
|
169
169
|
);
|
|
170
170
|
|
|
171
171
|
/**
|
package/js/hooks/useOrders.ts
CHANGED
|
@@ -73,7 +73,12 @@ const useOrdersBase =
|
|
|
73
73
|
queryOptions?: QueryOptions<CredentialOrder | CertificateOrder>,
|
|
74
74
|
) => {
|
|
75
75
|
const custom = useResourcesCustom({ ...props, filters, queryOptions });
|
|
76
|
-
const abortHandler = useSessionMutation({
|
|
76
|
+
const abortHandler = useSessionMutation({
|
|
77
|
+
mutationFn: useJoanieApi().user.orders.abort,
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
custom.methods.invalidate();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
77
82
|
const submitHandler = useSessionMutation({ mutationFn: useJoanieApi().user.orders.submit });
|
|
78
83
|
return {
|
|
79
84
|
...custom,
|
|
@@ -24,7 +24,7 @@ other {# remaining seats}
|
|
|
24
24
|
interface CourseProductItemFooterProps {
|
|
25
25
|
course: CourseLight;
|
|
26
26
|
product: CredentialProduct;
|
|
27
|
-
|
|
27
|
+
canPurchase: boolean;
|
|
28
28
|
orderGroups: OrderGroup[];
|
|
29
29
|
orderGroupsAvailable: OrderGroup[];
|
|
30
30
|
}
|
|
@@ -34,14 +34,14 @@ const CourseProductItemFooter = ({
|
|
|
34
34
|
product,
|
|
35
35
|
orderGroups,
|
|
36
36
|
orderGroupsAvailable,
|
|
37
|
-
|
|
37
|
+
canPurchase,
|
|
38
38
|
}: CourseProductItemFooterProps) => {
|
|
39
39
|
if (orderGroups.length === 0) {
|
|
40
40
|
return (
|
|
41
41
|
<PurchaseButton
|
|
42
42
|
course={course}
|
|
43
43
|
product={product}
|
|
44
|
-
disabled={!
|
|
44
|
+
disabled={!canPurchase}
|
|
45
45
|
buttonProps={{ fullWidth: true }}
|
|
46
46
|
/>
|
|
47
47
|
);
|
|
@@ -58,7 +58,7 @@ const CourseProductItemFooter = ({
|
|
|
58
58
|
<PurchaseButton
|
|
59
59
|
course={course}
|
|
60
60
|
product={product}
|
|
61
|
-
disabled={!
|
|
61
|
+
disabled={!canPurchase}
|
|
62
62
|
orderGroup={orderGroup}
|
|
63
63
|
buttonProps={{ fullWidth: true }}
|
|
64
64
|
/>
|
|
@@ -62,11 +62,11 @@ export interface CourseProductItemProps {
|
|
|
62
62
|
type HeaderProps = {
|
|
63
63
|
compact: boolean;
|
|
64
64
|
hasPurchased: boolean;
|
|
65
|
-
|
|
65
|
+
canPurchase: boolean;
|
|
66
66
|
order: Maybe<CredentialOrder>;
|
|
67
67
|
product: Product;
|
|
68
68
|
};
|
|
69
|
-
const Header = ({ product, order, hasPurchased,
|
|
69
|
+
const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
70
70
|
const intl = useIntl();
|
|
71
71
|
const formatDate = useDateFormat();
|
|
72
72
|
|
|
@@ -92,7 +92,7 @@ const Header = ({ product, order, hasPurchased, isPendingState, compact }: Heade
|
|
|
92
92
|
<strong className="product-widget__price h6">
|
|
93
93
|
{order?.state === OrderState.VALIDATED && <FormattedMessage {...messages.purchased} />}
|
|
94
94
|
{order?.state === OrderState.SUBMITTED && <FormattedMessage {...messages.pending} />}
|
|
95
|
-
{
|
|
95
|
+
{canPurchase && (
|
|
96
96
|
<FormattedNumber
|
|
97
97
|
currency={product.price_currency}
|
|
98
98
|
value={product.price}
|
|
@@ -171,7 +171,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
const order = productOrder as CredentialOrder;
|
|
174
|
-
const
|
|
174
|
+
const canPurchase = !order || order.state === OrderState.PENDING;
|
|
175
175
|
const hasPurchased = (order && order.state === OrderState.VALIDATED) ?? false;
|
|
176
176
|
|
|
177
177
|
const hasError = Boolean(productQueryStates.error);
|
|
@@ -228,7 +228,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
228
228
|
<Header
|
|
229
229
|
product={product}
|
|
230
230
|
order={order}
|
|
231
|
-
|
|
231
|
+
canPurchase={canPurchase}
|
|
232
232
|
hasPurchased={hasPurchased}
|
|
233
233
|
compact={compact}
|
|
234
234
|
/>
|
|
@@ -239,7 +239,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
239
239
|
product={product as CredentialProduct}
|
|
240
240
|
orderGroups={orderGroups}
|
|
241
241
|
orderGroupsAvailable={orderGroupsAvailable}
|
|
242
|
-
|
|
242
|
+
canPurchase={canPurchase}
|
|
243
243
|
/>
|
|
244
244
|
</footer>
|
|
245
245
|
</>
|