richie-education 2.28.2-dev39 → 2.28.2-dev58

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 (102) hide show
  1. package/.eslintrc.json +11 -2
  2. package/i18n/locales/ar-SA.json +209 -125
  3. package/i18n/locales/es-ES.json +210 -126
  4. package/i18n/locales/fa-IR.json +209 -125
  5. package/i18n/locales/fr-CA.json +209 -125
  6. package/i18n/locales/fr-FR.json +209 -125
  7. package/i18n/locales/ko-KR.json +209 -125
  8. package/i18n/locales/pt-PT.json +212 -128
  9. package/i18n/locales/ru-RU.json +209 -125
  10. package/i18n/locales/vi-VN.json +209 -125
  11. package/js/api/joanie.ts +14 -17
  12. package/js/api/lms/dummy.ts +1 -12
  13. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  14. package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
  15. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  16. package/js/components/ContractFrame/_styles.scss +6 -14
  17. package/js/components/CreditCardSelector/index.spec.tsx +7 -7
  18. package/js/components/CreditCardSelector/index.tsx +2 -2
  19. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  20. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  21. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  22. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  23. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  24. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  25. package/js/components/PaymentInterfaces/types.ts +5 -2
  26. package/js/components/PurchaseButton/index.spec.tsx +69 -37
  27. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  28. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  29. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  30. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  31. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  32. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  33. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  34. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  35. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  36. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  37. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
  38. package/js/components/SaleTunnel/_styles.scss +10 -1
  39. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  40. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  41. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  42. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  43. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  44. package/js/components/SignContractButton/index.spec.tsx +16 -20
  45. package/js/components/SignContractButton/index.tsx +3 -1
  46. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  47. package/js/hooks/useCreditCards/index.ts +49 -11
  48. package/js/hooks/useOrders/index.spec.tsx +322 -0
  49. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  50. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  51. package/js/hooks/useProductOrder/index.tsx +2 -2
  52. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  53. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  54. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  55. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  56. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  57. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  58. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  59. package/js/settings/settings.test.ts +11 -2
  60. package/js/translations/ar-SA.json +1 -1
  61. package/js/translations/es-ES.json +1 -1
  62. package/js/translations/fa-IR.json +1 -1
  63. package/js/translations/fr-CA.json +1 -1
  64. package/js/translations/fr-FR.json +1 -1
  65. package/js/translations/ko-KR.json +1 -1
  66. package/js/translations/pt-PT.json +1 -1
  67. package/js/translations/ru-RU.json +1 -1
  68. package/js/translations/vi-VN.json +1 -1
  69. package/js/types/Joanie.ts +49 -34
  70. package/js/utils/OrderHelper/index.ts +38 -42
  71. package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
  72. package/js/utils/test/factories/joanie.ts +36 -51
  73. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  74. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  75. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  76. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  77. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  78. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  79. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  80. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  81. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +4 -6
  82. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  83. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  84. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  85. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  86. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  87. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  88. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  89. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  90. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  91. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  92. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  93. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  94. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  95. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  96. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  97. package/package.json +27 -27
  98. package/scss/components/_index.scss +2 -1
  99. package/js/components/PaymentButton/_styles.scss +0 -27
  100. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  101. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  102. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
@@ -0,0 +1,322 @@
1
+ import fetchMock from 'fetch-mock';
2
+ import { QueryClientProvider } from '@tanstack/react-query';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { PropsWithChildren } from 'react';
5
+ import { act, renderHook } from '@testing-library/react';
6
+ import { waitFor } from '@testing-library/dom';
7
+ import { faker } from '@faker-js/faker';
8
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
10
+ import { SessionProvider } from 'contexts/SessionContext';
11
+ import { CertificateOrderFactory } from 'utils/test/factories/joanie';
12
+ import { Deferred } from 'utils/test/deferred';
13
+ import { HttpStatusCode } from 'utils/errors/HttpError';
14
+ import { useOmniscientOrder, useOmniscientOrders, useOrder, useOrders } from '.';
15
+
16
+ jest.mock('utils/context', () => ({
17
+ __esModule: true,
18
+ default: mockRichieContextFactory({
19
+ authentication: { backend: 'fonzie', endpoint: 'https://demo.endpoint' },
20
+ joanie_backend: { endpoint: 'https://joanie.endpoint' },
21
+ }).one(),
22
+ }));
23
+
24
+ describe('useOrders', () => {
25
+ const Wrapper = ({ children }: PropsWithChildren) => {
26
+ return (
27
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
28
+ <IntlProvider locale="en">
29
+ <SessionProvider>{children}</SessionProvider>
30
+ </IntlProvider>
31
+ </QueryClientProvider>
32
+ );
33
+ };
34
+
35
+ beforeEach(() => {
36
+ fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
37
+ fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
38
+ fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
39
+ });
40
+
41
+ afterEach(() => {
42
+ fetchMock.restore();
43
+ });
44
+
45
+ describe('omniscient', () => {
46
+ it('retrieves all the orders', async () => {
47
+ const orders = CertificateOrderFactory().many(5);
48
+ const deferrer = new Deferred<typeof orders>();
49
+ fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
50
+ overwriteRoutes: true,
51
+ });
52
+
53
+ const { result } = renderHook(useOmniscientOrders, {
54
+ wrapper: Wrapper,
55
+ });
56
+
57
+ await waitFor(() => {
58
+ expect(result.current.states.fetching).toBe(true);
59
+ });
60
+ expect(result.current.states.isPending).toBe(true);
61
+ expect(result.current.states.isFetched).toBe(false);
62
+ expect(result.current.states.creating).toBe(false);
63
+ expect(result.current.items).toEqual([]);
64
+
65
+ await act(async () => {
66
+ deferrer.resolve(orders);
67
+ });
68
+
69
+ await waitFor(() => {
70
+ expect(result.current.states.fetching).toBe(false);
71
+ });
72
+ expect(result.current.states.isPending).toBe(false);
73
+ expect(result.current.states.isFetched).toBe(true);
74
+ expect(result.current.states.creating).toBe(false);
75
+ expect(result.current.items).toEqual(orders);
76
+ });
77
+
78
+ it('retrieves a specific order omnisciently', async () => {
79
+ const orders = CertificateOrderFactory().many(5);
80
+ const deferrer = new Deferred<typeof orders>();
81
+ fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
82
+ overwriteRoutes: true,
83
+ });
84
+
85
+ const { result } = renderHook(() => useOmniscientOrder(orders[2].id), {
86
+ wrapper: Wrapper,
87
+ });
88
+
89
+ await waitFor(() => {
90
+ expect(result.current.states.fetching).toBe(true);
91
+ });
92
+ expect(result.current.states.isPending).toBe(true);
93
+ expect(result.current.states.isFetched).toBe(false);
94
+ expect(result.current.states.creating).toBe(false);
95
+ expect(result.current.item).toEqual(undefined);
96
+
97
+ await act(async () => {
98
+ deferrer.resolve(orders);
99
+ });
100
+
101
+ await waitFor(() => {
102
+ expect(result.current.states.fetching).toBe(false);
103
+ });
104
+ expect(result.current.states.isPending).toBe(false);
105
+ expect(result.current.states.isFetched).toBe(true);
106
+ expect(result.current.states.creating).toBe(false);
107
+ expect(result.current.item).toEqual(orders[2]);
108
+ });
109
+
110
+ it('cancels an order', async () => {
111
+ const deferrer = new Deferred();
112
+ const id = faker.string.uuid();
113
+ fetchMock.post(`https://joanie.endpoint/api/v1.0/orders/${id}/cancel/`, deferrer.promise);
114
+
115
+ const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
116
+ wrapper: Wrapper,
117
+ });
118
+
119
+ await waitFor(() => {
120
+ expect(result.current).not.toBeNull();
121
+ });
122
+
123
+ await act(async () => {
124
+ result.current.methods.cancel(id);
125
+ });
126
+
127
+ await waitFor(() => {
128
+ expect(result.current.states.isPending).toBe(true);
129
+ });
130
+ expect(result.current.states.fetching).toBe(false);
131
+ expect(result.current.states.cancelling).toBe(true);
132
+
133
+ await act(async () => {
134
+ deferrer.resolve(HttpStatusCode.OK);
135
+ });
136
+
137
+ await waitFor(() => {
138
+ expect(result.current.states.isPending).toBe(false);
139
+ });
140
+ expect(result.current.states.cancelling).toBe(false);
141
+ });
142
+
143
+ it('manages cancel mutation failure', async () => {
144
+ const id = faker.string.uuid();
145
+ fetchMock.post(
146
+ `https://joanie.endpoint/api/v1.0/orders/${id}/cancel/`,
147
+ HttpStatusCode.INTERNAL_SERVER_ERROR,
148
+ );
149
+
150
+ const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
151
+ wrapper: Wrapper,
152
+ });
153
+
154
+ await waitFor(() => {
155
+ expect(result.current).not.toBeNull();
156
+ });
157
+
158
+ expect(result.current.states.error).toBe(undefined);
159
+
160
+ await act(async () => {
161
+ await expect(result.current.methods.cancel(id)).rejects.toThrow('Internal Server Error');
162
+ });
163
+
164
+ expect(result.current.states.error).toBe('Cannot cancel the order.');
165
+ expect(result.current.states.isPending).toBe(false);
166
+ expect(result.current.states.cancelling).toBe(false);
167
+ });
168
+
169
+ it("set an order's payment method", async () => {
170
+ const deferrer = new Deferred();
171
+ const id = faker.string.uuid();
172
+ const creditCardId = faker.string.uuid();
173
+ fetchMock.post(
174
+ `https://joanie.endpoint/api/v1.0/orders/${id}/payment-method/`,
175
+ deferrer.promise,
176
+ );
177
+
178
+ const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
179
+ wrapper: Wrapper,
180
+ });
181
+
182
+ await waitFor(() => {
183
+ expect(result.current).not.toBeNull();
184
+ });
185
+
186
+ await act(async () => {
187
+ result.current.methods.set_payment_method({
188
+ id,
189
+ credit_card_id: creditCardId,
190
+ });
191
+ });
192
+
193
+ await waitFor(() => {
194
+ expect(result.current.states.isPending).toBe(true);
195
+ });
196
+ expect(result.current.states.fetching).toBe(false);
197
+ expect(result.current.states.settingPaymentMethod).toBe(true);
198
+
199
+ await act(async () => {
200
+ deferrer.resolve(HttpStatusCode.OK);
201
+ });
202
+
203
+ await waitFor(() => {
204
+ expect(result.current.states.isPending).toBe(false);
205
+ });
206
+ expect(result.current.states.settingPaymentMethod).toBe(false);
207
+ });
208
+
209
+ it('manages set payment method mutation failure', async () => {
210
+ const id = faker.string.uuid();
211
+ const creditCardId = faker.string.uuid();
212
+ fetchMock.post(
213
+ `https://joanie.endpoint/api/v1.0/orders/${id}/payment-method/`,
214
+ HttpStatusCode.INTERNAL_SERVER_ERROR,
215
+ );
216
+
217
+ const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
218
+ wrapper: Wrapper,
219
+ });
220
+
221
+ await waitFor(() => {
222
+ expect(result.current).not.toBeNull();
223
+ });
224
+
225
+ expect(result.current.states.error).toBe(undefined);
226
+
227
+ await act(async () => {
228
+ await expect(
229
+ result.current.methods.set_payment_method({ id, credit_card_id: creditCardId }),
230
+ ).rejects.toThrow('Internal Server Error');
231
+ });
232
+
233
+ expect(result.current.states.error).toBe("Cannot set the order's payment method.");
234
+ expect(result.current.states.isPending).toBe(false);
235
+ expect(result.current.states.cancelling).toBe(false);
236
+ });
237
+ });
238
+
239
+ describe('non-omniscient', () => {
240
+ it('retrieves all the orders', async () => {
241
+ const orders = CertificateOrderFactory().many(5);
242
+ const deferrer = new Deferred<typeof orders>();
243
+ fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
244
+ overwriteRoutes: true,
245
+ });
246
+
247
+ const { result } = renderHook(useOrders, {
248
+ wrapper: Wrapper,
249
+ });
250
+
251
+ await waitFor(() => {
252
+ expect(result.current.states.fetching).toBe(true);
253
+ });
254
+ expect(result.current.states.isPending).toBe(true);
255
+ expect(result.current.states.isFetched).toBe(false);
256
+ expect(result.current.states.creating).toBe(false);
257
+ expect(result.current.items).toEqual([]);
258
+
259
+ await act(async () => {
260
+ deferrer.resolve(orders);
261
+ });
262
+
263
+ await waitFor(() => {
264
+ expect(result.current.states.fetching).toBe(false);
265
+ });
266
+ expect(result.current.states.isPending).toBe(false);
267
+ expect(result.current.states.isFetched).toBe(true);
268
+ expect(result.current.states.creating).toBe(false);
269
+ expect(result.current.items).toEqual(orders);
270
+ });
271
+
272
+ it('retrieves a specific order', async () => {
273
+ const order = CertificateOrderFactory().one();
274
+ const deferrer = new Deferred<typeof order>();
275
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, deferrer.promise);
276
+
277
+ const { result } = renderHook(() => useOrder(order.id), {
278
+ wrapper: Wrapper,
279
+ });
280
+
281
+ await waitFor(() => {
282
+ expect(result.current.states.fetching).toBe(true);
283
+ });
284
+ expect(result.current.states.isPending).toBe(true);
285
+ expect(result.current.states.isFetched).toBe(false);
286
+ expect(result.current.states.creating).toBe(false);
287
+ expect(result.current.item).toEqual(undefined);
288
+
289
+ await act(async () => {
290
+ deferrer.resolve(order);
291
+ });
292
+
293
+ await waitFor(() => {
294
+ expect(result.current.states.fetching).toBe(false);
295
+ });
296
+ expect(result.current.states.isPending).toBe(false);
297
+ expect(result.current.states.isFetched).toBe(true);
298
+ expect(result.current.states.creating).toBe(false);
299
+ expect(result.current.item).toEqual(order);
300
+ });
301
+
302
+ it('has a method to cancel an order', async () => {
303
+ const { result } = renderHook(() => useOrders(undefined, { enabled: false }), {
304
+ wrapper: Wrapper,
305
+ });
306
+
307
+ await waitFor(() => {
308
+ expect(result.current.methods.cancel).not.toBeUndefined();
309
+ });
310
+ });
311
+
312
+ it('has a method to set a payment method', async () => {
313
+ const { result } = renderHook(() => useOrders(undefined, { enabled: false }), {
314
+ wrapper: Wrapper,
315
+ });
316
+
317
+ await waitFor(() => {
318
+ expect(result.current.methods.set_payment_method).not.toBeUndefined();
319
+ });
320
+ });
321
+ });
322
+ });
@@ -1,4 +1,5 @@
1
- import { defineMessages } from 'react-intl';
1
+ import { defineMessages, useIntl } from 'react-intl';
2
+ import { useQueryClient } from '@tanstack/react-query';
2
3
  import {
3
4
  API,
4
5
  CertificateOrder,
@@ -12,7 +13,7 @@ import {
12
13
  } from 'types/Joanie';
13
14
  import { useJoanieApi } from 'contexts/JoanieApiContext';
14
15
  import { useSessionMutation } from 'utils/react-query/useSessionMutation';
15
- import { QueryOptions, useResource, useResourcesCustom, UseResourcesProps } from './useResources';
16
+ import { QueryOptions, useResource, useResourcesCustom, UseResourcesProps } from '../useResources';
16
17
 
17
18
  export type OrderResourcesQuery = PaginatedResourceQuery & {
18
19
  course_code?: CourseLight['code'];
@@ -36,6 +37,16 @@ const messages = defineMessages({
36
37
  description: 'Error message shown to the user when no orders matches.',
37
38
  defaultMessage: 'Cannot find the orders.',
38
39
  },
40
+ errorCancel: {
41
+ id: 'hooks.useOrders.errorCancel',
42
+ description: 'Error message shown to the user when cancel mutation failed.',
43
+ defaultMessage: 'Cannot cancel the order.',
44
+ },
45
+ errorSetPaymentMethod: {
46
+ id: 'hooks.useOrders.errorSetPaymentMethod',
47
+ description: 'Error message shown to the user when set payment method mutation failed.',
48
+ defaultMessage: "Cannot set the order's payment method.",
49
+ },
39
50
  });
40
51
 
41
52
  function omniscientFiltering(
@@ -73,25 +84,40 @@ const useOrdersBase =
73
84
  filters?: OrderResourcesQuery,
74
85
  queryOptions?: QueryOptions<CredentialOrder | CertificateOrder>,
75
86
  ) => {
87
+ const intl = useIntl();
76
88
  const custom = useResourcesCustom({ ...props, filters, queryOptions });
77
- const abortHandler = useSessionMutation({
78
- mutationFn: useJoanieApi().user.orders.abort,
79
- onSuccess: () => {
80
- custom.methods.invalidate();
81
- },
89
+ const queryClient = useQueryClient();
90
+ const api = props.apiInterface();
91
+ const onSuccess = async () => {
92
+ custom.methods.setError(undefined);
93
+ await custom.methods.invalidate();
94
+ props.onMutationSuccess?.(queryClient);
95
+ };
96
+ const cancelHandler = useSessionMutation({
97
+ mutationFn: api.cancel,
98
+ onSuccess,
99
+ onError: () => custom.methods.setError(intl.formatMessage(messages.errorCancel)),
82
100
  });
83
- const submitHandler = useSessionMutation({
84
- mutationFn: useJoanieApi().user.orders.submit,
85
- onSuccess: () => {
86
- custom.methods.invalidate();
87
- },
101
+ const setPaymentMethodHandler = useSessionMutation({
102
+ mutationFn: api.set_payment_method,
103
+ onSuccess,
104
+ onError: () => custom.methods.setError(intl.formatMessage(messages.errorSetPaymentMethod)),
88
105
  });
106
+
89
107
  return {
90
108
  ...custom,
91
109
  methods: {
92
110
  ...custom.methods,
93
- abort: abortHandler.mutateAsync,
94
- submit: submitHandler.mutateAsync,
111
+ cancel: cancelHandler.mutateAsync,
112
+ set_payment_method: setPaymentMethodHandler.mutateAsync,
113
+ },
114
+ states: {
115
+ ...custom.states,
116
+ cancelling: cancelHandler.isPending,
117
+ settingPaymentMethod: setPaymentMethodHandler.isPending,
118
+ isPending: [custom.states, cancelHandler, setPaymentMethodHandler].some(
119
+ (value) => value?.isPending,
120
+ ),
95
121
  },
96
122
  };
97
123
  };
@@ -9,7 +9,7 @@ import { CourseLightFactory, CredentialOrderFactory } from 'utils/test/factories
9
9
  import { SessionProvider } from 'contexts/SessionContext';
10
10
  import { Deferred } from 'utils/test/deferred';
11
11
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
12
- import { ACTIVE_ORDER_STATES } from 'types/Joanie';
12
+ import { NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
13
13
  import useProductOrder from '.';
14
14
 
15
15
  jest.mock('utils/context', () => ({
@@ -45,69 +45,86 @@ describe('useProductOrder', () => {
45
45
  );
46
46
  };
47
47
 
48
- it.each(ACTIVE_ORDER_STATES)(
49
- 'should retrieves the last order when order.state is %s',
50
- async (currentState) => {
51
- // the most recent order of accepted state will be return
52
- const order = CredentialOrderFactory({
53
- state: currentState,
54
- created_on: new Date().toISOString(),
55
- course: CourseLightFactory({ code: '00000' }).one(),
56
- }).one();
57
- const ordersByState = ACTIVE_ORDER_STATES.filter((state) => state !== currentState).map(
58
- (state) =>
59
- CredentialOrderFactory({
60
- state,
61
- created_on: faker.date.past({ years: 1 }).toISOString(),
62
- course: CourseLightFactory({ code: '00000' }).one(),
63
- product_id: order.product_id,
64
- }).one(),
65
- );
66
- ordersByState.push(order);
48
+ it.each([
49
+ OrderState.DRAFT,
50
+ OrderState.ASSIGNED,
51
+ OrderState.TO_SIGN,
52
+ OrderState.SIGNING,
53
+ OrderState.TO_SAVE_PAYMENT_METHOD,
54
+ OrderState.PENDING,
55
+ OrderState.PENDING_PAYMENT,
56
+ OrderState.NO_PAYMENT,
57
+ OrderState.FAILED_PAYMENT,
58
+ OrderState.COMPLETED,
59
+ ])('should retrieves the last order when order.state is %s', async (currentState) => {
60
+ // the most recent order of accepted state will be return
61
+ const order = CredentialOrderFactory({
62
+ state: currentState,
63
+ created_on: new Date().toISOString(),
64
+ course: CourseLightFactory({ code: '00000' }).one(),
65
+ }).one();
66
+ const ordersByState = NOT_CANCELED_ORDER_STATES.filter((state) => state !== currentState).map(
67
+ (state) =>
68
+ CredentialOrderFactory({
69
+ state,
70
+ created_on: faker.date.past({ years: 1 }).toISOString(),
71
+ course: CourseLightFactory({ code: '00000' }).one(),
72
+ product_id: order.product_id,
73
+ }).one(),
74
+ );
75
+ ordersByState.push(order);
67
76
 
68
- const responseDeferred = new Deferred();
69
- fetchMock.get(
70
- `https://joanie.endpoint/api/v1.0/orders/?course_code=00000&product_id=${order.product_id}&state=pending&state=validated&state=submitted`,
71
- responseDeferred.promise,
72
- );
77
+ const responseDeferred = new Deferred();
78
+ const url =
79
+ 'https://joanie.endpoint/api/v1.0/orders/' +
80
+ '?course_code=00000' +
81
+ `&product_id=${order.product_id}` +
82
+ `&state=${OrderState.PENDING}` +
83
+ `&state=${OrderState.PENDING_PAYMENT}` +
84
+ `&state=${OrderState.NO_PAYMENT}` +
85
+ `&state=${OrderState.FAILED_PAYMENT}` +
86
+ `&state=${OrderState.COMPLETED}` +
87
+ `&state=${OrderState.DRAFT}` +
88
+ `&state=${OrderState.ASSIGNED}` +
89
+ `&state=${OrderState.TO_SIGN}` +
90
+ `&state=${OrderState.SIGNING}` +
91
+ `&state=${OrderState.TO_SAVE_PAYMENT_METHOD}`;
92
+ fetchMock.get(url, responseDeferred.promise);
73
93
 
74
- const { result } = renderHook(
75
- () => useProductOrder({ productId: order.product_id, courseCode: '00000' }),
76
- {
77
- wrapper: Wrapper,
78
- },
79
- );
94
+ const { result } = renderHook(
95
+ () => useProductOrder({ productId: order.product_id, courseCode: '00000' }),
96
+ {
97
+ wrapper: Wrapper,
98
+ },
99
+ );
80
100
 
81
- await waitFor(() => {
82
- expect(result.current.states.fetching).toBe(true);
83
- expect(result.current.item).toBeUndefined();
84
- });
85
- nbApiCalls += 1; // call orders from useProductOrder
86
- const calledUrls = fetchMock.calls().map((call) => call[0]);
87
- expect(calledUrls).toHaveLength(nbApiCalls);
88
- expect(calledUrls).toContain(
89
- `https://joanie.endpoint/api/v1.0/orders/?course_code=00000&product_id=${order.product_id}&state=pending&state=validated&state=submitted`,
90
- );
91
- expect(result.current.states.creating).toBe(false);
92
- expect(result.current.states.deleting).toBeUndefined();
93
- expect(result.current.states.updating).toBeUndefined();
94
- expect(result.current.states.isPending).toBe(true);
95
- expect(result.current.states.error).toBe(undefined);
101
+ await waitFor(() => {
102
+ expect(result.current.states.fetching).toBe(true);
103
+ expect(result.current.item).toBeUndefined();
104
+ });
105
+ nbApiCalls += 1; // call orders from useProductOrder
106
+ const calledUrls = fetchMock.calls().map((call) => call[0]);
107
+ expect(calledUrls).toHaveLength(nbApiCalls);
108
+ expect(calledUrls).toContain(url);
109
+ expect(result.current.states.creating).toBe(false);
110
+ expect(result.current.states.deleting).toBeUndefined();
111
+ expect(result.current.states.updating).toBeUndefined();
112
+ expect(result.current.states.isPending).toBe(true);
113
+ expect(result.current.states.error).toBe(undefined);
96
114
 
97
- await act(async () => {
98
- responseDeferred.resolve(ordersByState);
99
- });
115
+ await act(async () => {
116
+ responseDeferred.resolve(ordersByState);
117
+ });
100
118
 
101
- await waitFor(() => {
102
- expect(result.current.states.fetching).toBe(false);
103
- expect(result.current.item).toEqual(order);
104
- });
119
+ await waitFor(() => {
120
+ expect(result.current.states.fetching).toBe(false);
121
+ expect(result.current.item).toEqual(order);
122
+ });
105
123
 
106
- expect(result.current.states.creating).toBe(false);
107
- expect(result.current.states.deleting).toBeUndefined();
108
- expect(result.current.states.updating).toBeUndefined();
109
- expect(result.current.states.isPending).toBe(false);
110
- expect(result.current.states.error).toBe(undefined);
111
- },
112
- );
124
+ expect(result.current.states.creating).toBe(false);
125
+ expect(result.current.states.deleting).toBeUndefined();
126
+ expect(result.current.states.updating).toBeUndefined();
127
+ expect(result.current.states.isPending).toBe(false);
128
+ expect(result.current.states.error).toBe(undefined);
129
+ });
113
130
  });
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useOrders } from 'hooks/useOrders';
3
- import { ACTIVE_ORDER_STATES } from 'types/Joanie';
3
+ import { NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
4
4
 
5
5
  interface UseProductOrderProps {
6
6
  courseCode?: string;
@@ -12,7 +12,7 @@ const useProductOrder = ({ courseCode, enrollmentId, productId }: UseProductOrde
12
12
  course_code: courseCode,
13
13
  enrollment_id: enrollmentId,
14
14
  product_id: productId,
15
- state: ACTIVE_ORDER_STATES,
15
+ state: NOT_CANCELED_ORDER_STATES,
16
16
  });
17
17
 
18
18
  const order = useMemo(() => {
@@ -59,6 +59,7 @@ const emptyArray: never[] = [];
59
59
  * @param queryOptions - Pass custom options to react-query.
60
60
  * @param localized - Is the resource local-dependent ? If so, the query will be invalidated on locale change.
61
61
  * @param resourceMessages - Custom messages to use for this resource.
62
+ * @param onMutationSuccess - Custom callback triggered on mutation success.
62
63
  */
63
64
  export const useResourcesRoot = <
64
65
  TData extends Resource,
@@ -23,7 +23,7 @@ describe('CreditCardBrandLogo', () => {
23
23
  render(<CreditCardBrandLogo creditCard={creditCard} />);
24
24
  expect(screen.getByRole('presentation')).toHaveAttribute(
25
25
  'src',
26
- '/static/richie/images/components/DashboardCreditCardsManagement/logo_CB.svg',
26
+ '/static/richie/images/components/DashboardCreditCardsManagement/logo_cb.svg',
27
27
  );
28
28
  });
29
29
  });
@@ -8,8 +8,10 @@ export const CreditCardBrandLogo = ({
8
8
  creditCard: CreditCard;
9
9
  variant?: 'default' | 'inline';
10
10
  }) => {
11
- const creditCardBrand = Object.values<string>(CreditCardBrand).includes(creditCard.brand)
12
- ? creditCard.brand
11
+ const creditCardBrand = Object.values<string>(CreditCardBrand).includes(
12
+ creditCard.brand.toLowerCase(),
13
+ )
14
+ ? creditCard.brand.toLowerCase()
13
15
  : CreditCardBrand.CB;
14
16
 
15
17
  return (
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
3
3
  import { IntlProvider } from 'react-intl';
4
4
  import { PropsWithChildren } from 'react';
5
5
  import { QueryClientProvider } from '@tanstack/react-query';
6
+ import { CunninghamProvider } from '@openfun/cunningham-react';
6
7
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
8
  import JoanieApiProvider from 'contexts/JoanieApiContext';
8
9
 
@@ -50,11 +51,13 @@ jest.mock('pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/
50
51
  describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
51
52
  const Wrapper = ({ children }: PropsWithChildren) => {
52
53
  return (
53
- <IntlProvider locale="en">
54
- <QueryClientProvider client={createTestQueryClient({ user: true })}>
55
- <JoanieApiProvider>{children}</JoanieApiProvider>
56
- </QueryClientProvider>
57
- </IntlProvider>
54
+ <CunninghamProvider>
55
+ <IntlProvider locale="en">
56
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
57
+ <JoanieApiProvider>{children}</JoanieApiProvider>
58
+ </QueryClientProvider>
59
+ </IntlProvider>
60
+ </CunninghamProvider>
58
61
  );
59
62
  };
60
63