richie-education 2.25.0-b2.dev77 → 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 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, useState } from 'react';
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 Wrapper = ({
92
- client = createTestQueryClient({ user: true }),
93
- children,
97
+ const SaleTunnelWrapper = ({
94
98
  product,
95
99
  orderGroup,
96
- }: PropsWithChildren<{
97
- client?: QueryClient;
98
- product: CredentialProduct | CertificateProduct;
99
- orderGroup?: OrderGroup;
100
- }>) => {
101
- const [order, setOrder] = useState<Maybe<Order>>();
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, setOrder, orderGroup]);
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
- <SaleTunnelContext.Provider value={context}>{children}</SaleTunnelContext.Provider>
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; // fetch order for useProductOrder
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; // refetch omniscient orders
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
- expect(fetchMock.calls()).toHaveLength(PAYMENT_SETTINGS.pollLimit);
659
- expect(fetchMock.lastUrl()).toBe(
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(JSON.parse(fetchMock.lastOptions()!.body!.toString())).toEqual({
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, setOrder, orderGroup } = useSaleTunnelContext();
103
- const { item: order } = useProductOrder({
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
- orderManager.methods.submit(
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
- orderManager.methods.create(payload, {
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
- orderManager.methods.abort({ id: payment!.order_id, payment_id: payment!.payment_id });
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
- orderManager.methods.abort({
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
- orderManager.methods
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
  }}
@@ -12,7 +12,6 @@ interface SaleTunnelContextBase {
12
12
  product: CredentialProduct | CertificateProduct;
13
13
  orderGroup?: OrderGroup;
14
14
  order?: Order;
15
- setOrder: (order: Order) => void;
16
15
  key: string;
17
16
  enrollment?: Enrollment;
18
17
  course?: CourseLight;
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
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 { Maybe } from 'types/utils';
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
- const [order, setOrder] = useState<Maybe<Order>>();
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, setOrder]);
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, setOrder, key, course, enrollment, orderGroup],
168
+ [product, order, key, course, enrollment, orderGroup],
169
169
  );
170
170
 
171
171
  /**
@@ -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({ mutationFn: useJoanieApi().user.orders.abort });
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
- isPendingState: boolean;
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
- isPendingState,
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={!isPendingState}
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={!isPendingState}
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
- isPendingState: boolean;
65
+ canPurchase: boolean;
66
66
  order: Maybe<CredentialOrder>;
67
67
  product: Product;
68
68
  };
69
- const Header = ({ product, order, hasPurchased, isPendingState, compact }: HeaderProps) => {
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
- {isPendingState && (
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 isPendingState = !order || order.state === OrderState.PENDING;
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
- isPendingState={isPendingState}
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
- isPendingState={isPendingState}
242
+ canPurchase={canPurchase}
243
243
  />
244
244
  </footer>
245
245
  </>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev77",
3
+ "version": "2.25.0-b2.dev80",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -52,17 +52,17 @@
52
52
  "@openfun/cunningham-tokens": "2.1.0",
53
53
  "@sentry/browser": "7.100.1",
54
54
  "@sentry/types": "7.100.1",
55
- "@storybook/addon-actions": "7.6.13",
56
- "@storybook/addon-essentials": "7.6.13",
57
- "@storybook/addon-interactions": "7.6.13",
58
- "@storybook/addon-links": "7.6.13",
59
- "@storybook/react": "7.6.13",
60
- "@storybook/react-webpack5": "7.6.13",
55
+ "@storybook/addon-actions": "7.6.14",
56
+ "@storybook/addon-essentials": "7.6.14",
57
+ "@storybook/addon-interactions": "7.6.14",
58
+ "@storybook/addon-links": "7.6.14",
59
+ "@storybook/react": "7.6.14",
60
+ "@storybook/react-webpack5": "7.6.14",
61
61
  "@storybook/testing-library": "0.2.2",
62
- "@tanstack/query-core": "5.18.1",
63
- "@tanstack/query-sync-storage-persister": "5.18.1",
64
- "@tanstack/react-query": "5.18.1",
65
- "@tanstack/react-query-persist-client": "5.18.1",
62
+ "@tanstack/query-core": "5.20.1",
63
+ "@tanstack/query-sync-storage-persister": "5.20.1",
64
+ "@tanstack/react-query": "5.20.1",
65
+ "@tanstack/react-query-persist-client": "5.20.1",
66
66
  "@testing-library/dom": "9.3.4",
67
67
  "@testing-library/jest-dom": "6.4.2",
68
68
  "@testing-library/react": "14.2.1",
@@ -127,7 +127,7 @@
127
127
  "react-router-dom": "6.22.0",
128
128
  "sass": "1.70.0",
129
129
  "source-map-loader": "5.0.0",
130
- "storybook": "7.6.13",
130
+ "storybook": "7.6.14",
131
131
  "tsconfig-paths-webpack-plugin": "4.1.0",
132
132
  "typescript": "5.3.3",
133
133
  "uuid": "9.0.1",