richie-education 2.28.2-dev26 → 2.28.2-dev53

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 (89) hide show
  1. package/js/api/joanie.ts +42 -17
  2. package/js/api/lms/dummy.ts +1 -12
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  5. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  6. package/js/components/ContractFrame/_styles.scss +6 -14
  7. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
  8. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +17 -24
  9. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  10. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  11. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  12. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  13. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  14. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  15. package/js/components/PaymentInterfaces/types.ts +5 -2
  16. package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
  17. package/js/components/PaymentScheduleGrid/index.tsx +50 -70
  18. package/js/components/PurchaseButton/index.spec.tsx +84 -37
  19. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  20. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  21. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  22. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +80 -27
  23. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
  24. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  25. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  26. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  27. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
  28. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
  29. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  30. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  31. package/js/components/SaleTunnel/_styles.scss +16 -5
  32. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  33. package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
  34. package/js/components/SaleTunnel/index.full-process.spec.tsx +116 -48
  35. package/js/components/SaleTunnel/index.spec.tsx +334 -717
  36. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  37. package/js/components/SignContractButton/index.spec.tsx +16 -20
  38. package/js/components/SignContractButton/index.tsx +3 -1
  39. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  40. package/js/hooks/useCreditCards/index.ts +49 -11
  41. package/js/hooks/useOrders/index.spec.tsx +322 -0
  42. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  43. package/js/hooks/usePaymentSchedule.tsx +23 -0
  44. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  45. package/js/hooks/useProductOrder/index.tsx +2 -2
  46. package/js/hooks/useResources/useResourcesRoot.ts +4 -3
  47. package/js/index.tsx +2 -0
  48. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  49. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  50. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  51. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  53. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  54. package/js/settings/settings.test.ts +11 -2
  55. package/js/types/Joanie.ts +77 -31
  56. package/js/utils/OrderHelper/index.ts +47 -38
  57. package/js/utils/test/factories/joanie.ts +66 -68
  58. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  59. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  60. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +114 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +99 -12
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +126 -0
  67. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +209 -0
  68. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
  70. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
  71. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  72. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  73. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  74. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  77. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  78. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  79. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  83. package/package.json +2 -1
  84. package/scss/components/_index.scss +4 -2
  85. package/js/components/PaymentButton/_styles.scss +0 -27
  86. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -338
  87. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  88. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
  89. /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
package/js/api/joanie.ts CHANGED
@@ -15,7 +15,8 @@ import context from 'utils/context';
15
15
  import { JOANIE_API_VERSION } from 'settings';
16
16
  import { ResourcesQuery } from 'hooks/useResources';
17
17
  import { ObjectHelper } from 'utils/ObjectHelper';
18
- import { Maybe } from 'types/utils';
18
+ import { Maybe, Nullable } from 'types/utils';
19
+ import { PaymentSchedule } from 'types/Joanie';
19
20
  import { checkStatus, getFileFromResponse } from './utils';
20
21
 
21
22
  /*
@@ -78,9 +79,9 @@ export const getRoutes = () => {
78
79
  },
79
80
  creditCards: {
80
81
  get: `${baseUrl}/credit-cards/:id/`,
81
- create: `${baseUrl}/credit-cards/`,
82
82
  update: `${baseUrl}/credit-cards/:id/`,
83
83
  delete: `${baseUrl}/credit-cards/:id/`,
84
+ tokenize: `${baseUrl}/credit-cards/tokenize-card/`,
84
85
  },
85
86
  addresses: {
86
87
  get: `${baseUrl}/addresses/:id/`,
@@ -89,14 +90,15 @@ export const getRoutes = () => {
89
90
  delete: `${baseUrl}/addresses/:id/`,
90
91
  },
91
92
  orders: {
92
- abort: `${baseUrl}/orders/:id/abort/`,
93
+ cancel: `${baseUrl}/orders/:id/cancel/`,
93
94
  create: `${baseUrl}/orders/`,
94
- submit: `${baseUrl}/orders/:id/submit/`,
95
95
  get: `${baseUrl}/orders/:id/`,
96
96
  invoice: {
97
97
  download: `${baseUrl}/orders/:id/invoice/`,
98
98
  },
99
99
  submit_for_signature: `${baseUrl}/orders/:id/submit_for_signature/`,
100
+ submit_installment_payment: `${baseUrl}/orders/:id/submit_installment_payment/`,
101
+ set_payment_method: `${baseUrl}/orders/:id/payment-method/`,
100
102
  },
101
103
  certificates: {
102
104
  download: `${baseUrl}/certificates/:id/download/`,
@@ -141,6 +143,9 @@ export const getRoutes = () => {
141
143
  },
142
144
  products: {
143
145
  get: `${baseUrl}/courses/:course_id/products/:id/`,
146
+ paymentSchedule: {
147
+ get: `${baseUrl}/courses/:course_id/products/:id/payment-schedule/`,
148
+ },
144
149
  },
145
150
  orders: {
146
151
  get: `${baseUrl}/courses/:course_id/orders/:id/`,
@@ -224,11 +229,6 @@ const API = (): Joanie.API => {
224
229
  get: async (filters?: ResourcesQuery) => {
225
230
  return fetchWithJWT(buildApiUrl(ROUTES.user.creditCards.get, filters)).then(checkStatus);
226
231
  },
227
- create: async (creditCard) =>
228
- fetchWithJWT(ROUTES.user.creditCards.create, {
229
- method: 'POST',
230
- body: JSON.stringify(creditCard),
231
- }).then(checkStatus),
232
232
  update: async ({ id, ...creditCard }) => {
233
233
  return fetchWithJWT(ROUTES.user.creditCards.update.replace(':id', id), {
234
234
  method: 'PUT',
@@ -239,6 +239,8 @@ const API = (): Joanie.API => {
239
239
  fetchWithJWT(ROUTES.user.creditCards.delete.replace(':id', id), {
240
240
  method: 'DELETE',
241
241
  }).then(checkStatus),
242
+ tokenize: async () =>
243
+ fetchWithJWT(ROUTES.user.creditCards.tokenize, { method: 'POST' }).then(checkStatus),
242
244
  },
243
245
  addresses: {
244
246
  get: (id?: string) => {
@@ -260,10 +262,9 @@ const API = (): Joanie.API => {
260
262
  }).then(checkStatus),
261
263
  },
262
264
  orders: {
263
- abort: async ({ id, payment_id }) => {
264
- return fetchWithJWT(ROUTES.user.orders.abort.replace(':id', id), {
265
+ cancel: async (id) => {
266
+ return fetchWithJWT(ROUTES.user.orders.cancel.replace(':id', id), {
265
267
  method: 'POST',
266
- body: payment_id ? JSON.stringify({ payment_id }) : undefined,
267
268
  }).then(checkStatus);
268
269
  },
269
270
  create: async (payload) =>
@@ -271,11 +272,6 @@ const API = (): Joanie.API => {
271
272
  method: 'POST',
272
273
  body: JSON.stringify(payload),
273
274
  }).then(checkStatus),
274
- submit: async ({ id, ...payload }) =>
275
- fetchWithJWT(ROUTES.user.orders.submit.replace(':id', id), {
276
- method: 'PATCH',
277
- body: JSON.stringify(payload),
278
- }).then(checkStatus),
279
275
  get: async (filters) => {
280
276
  return fetchWithJWT(buildApiUrl(ROUTES.user.orders.get, filters)).then(checkStatus);
281
277
  },
@@ -291,6 +287,16 @@ const API = (): Joanie.API => {
291
287
  fetchWithJWT(ROUTES.user.orders.submit_for_signature.replace(':id', id), {
292
288
  method: 'POST',
293
289
  }).then(checkStatus),
290
+ submit_installment_payment: async (id, payload) =>
291
+ fetchWithJWT(ROUTES.user.orders.submit_installment_payment.replace(':id', id), {
292
+ method: 'POST',
293
+ body: JSON.stringify(payload),
294
+ }).then(checkStatus),
295
+ set_payment_method: async ({ id, ...payload }) =>
296
+ fetchWithJWT(ROUTES.user.orders.set_payment_method.replace(':id', id), {
297
+ method: 'POST',
298
+ body: JSON.stringify(payload),
299
+ }).then(checkStatus),
294
300
  },
295
301
  enrollments: {
296
302
  create: async (payload) =>
@@ -418,6 +424,25 @@ const API = (): Joanie.API => {
418
424
 
419
425
  return fetchWithJWT(buildApiUrl(ROUTES.courses.products.get, filters)).then(checkStatus);
420
426
  },
427
+ paymentSchedule: {
428
+ get: async (
429
+ filters?: Joanie.CourseProductQueryFilters,
430
+ ): Promise<Nullable<PaymentSchedule>> => {
431
+ if (!filters) {
432
+ throw new Error(
433
+ 'A course code and a product id are required to fetch a course product',
434
+ );
435
+ } else if (!filters.course_id) {
436
+ throw new Error('A course code is required to fetch a course product');
437
+ } else if (!filters.id) {
438
+ throw new Error('A product id is required to fetch a course product');
439
+ }
440
+
441
+ return fetchWithJWT(
442
+ buildApiUrl(ROUTES.courses.products.paymentSchedule.get, filters),
443
+ ).then(checkStatus);
444
+ },
445
+ },
421
446
  },
422
447
  orders: {
423
448
  get: async (filters?: Joanie.CourseOrderResourceQuery) => {
@@ -68,7 +68,7 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
68
68
 
69
69
  const dummyOpenEdxApiProfile: OpenEdxApiProfile = {
70
70
  username: 'j_do',
71
- name: 'John Do',
71
+ name: 'John Doe',
72
72
  email: 'j.do@whois.net',
73
73
  country: 'fr',
74
74
  level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
@@ -112,17 +112,6 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
112
112
  account: {
113
113
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
114
114
  get: (username: string): Promise<OpenEdxApiProfile> => {
115
- return Promise.resolve({
116
- username: 'j_do',
117
- name: 'John Do',
118
- email: 'j.do@whois.net',
119
- country: 'fr',
120
- level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
121
- gender: OpenEdxGender.MALE,
122
- year_of_birth: '1971',
123
- 'pref-lang': OpenEdxLanguageIsoCode.ENGLISH,
124
- language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
125
- } as OpenEdxApiProfile);
126
115
  return Promise.resolve(dummyOpenEdxApiProfile);
127
116
  },
128
117
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -4,6 +4,7 @@ import fetchMock from 'fetch-mock';
4
4
  import { render, screen, waitFor, act } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { PropsWithChildren } from 'react';
7
+ import { CunninghamProvider } from '@openfun/cunningham-react';
7
8
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
9
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
9
10
  import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
@@ -33,11 +34,13 @@ jest.mock('settings', () => ({
33
34
  describe('<AbstractContractFrame />', () => {
34
35
  const Wrapper = ({ children }: PropsWithChildren) => {
35
36
  return (
36
- <QueryClientProvider client={createTestQueryClient({ user: true })}>
37
- <IntlProvider locale="en">
38
- <JoanieSessionProvider>{children}</JoanieSessionProvider>
39
- </IntlProvider>
40
- </QueryClientProvider>
37
+ <CunninghamProvider>
38
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
39
+ <IntlProvider locale="en">
40
+ <JoanieSessionProvider>{children}</JoanieSessionProvider>
41
+ </IntlProvider>
42
+ </QueryClientProvider>
43
+ </CunninghamProvider>
41
44
  );
42
45
  };
43
46
 
@@ -78,7 +81,7 @@ describe('<AbstractContractFrame />', () => {
78
81
  expect(await screen.findByTestId('dashboard-contract-frame')).toBeInTheDocument();
79
82
 
80
83
  const user = userEvent.setup();
81
- await user.click(screen.getByRole('button', { name: 'Close dialog' }));
84
+ await user.click(screen.getByRole('button', { name: 'close' }));
82
85
  expect(mockOnClose).toHaveBeenCalled();
83
86
  });
84
87
 
@@ -115,6 +118,7 @@ describe('<AbstractContractFrame />', () => {
115
118
  const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
116
119
  const mockOnDone = jest.fn();
117
120
  const mockOnClose = jest.fn();
121
+ fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
118
122
 
119
123
  await act(async () => {
120
124
  render(
@@ -176,6 +180,7 @@ describe('<AbstractContractFrame />', () => {
176
180
  const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
177
181
  const mockOnDone = jest.fn();
178
182
  const mockOnClose = jest.fn();
183
+ fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
179
184
 
180
185
  await act(async () => {
181
186
  render(
@@ -222,7 +227,7 @@ describe('<AbstractContractFrame />', () => {
222
227
  // have been called
223
228
  await expectBannerError('An error happened while verifying signature. Please come back later.');
224
229
  expect(mockOnDone).not.toHaveBeenCalled();
225
- button = screen.getByRole('button', { name: 'Close dialog' });
230
+ button = screen.getByRole('button', { name: 'close' });
226
231
  await user.click(button);
227
232
 
228
233
  expect(mockOnClose).toHaveBeenCalledTimes(1);
@@ -237,6 +242,7 @@ describe('<AbstractContractFrame />', () => {
237
242
  const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
238
243
  const mockOnDone = jest.fn();
239
244
  const mockOnClose = jest.fn();
245
+ fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
240
246
 
241
247
  await act(async () => {
242
248
  render(
@@ -285,7 +291,7 @@ describe('<AbstractContractFrame />', () => {
285
291
  'The signature is taking more time than expected ... please come back later.',
286
292
  );
287
293
  expect(mockOnDone).not.toHaveBeenCalled();
288
- button = screen.getByRole('button', { name: 'Close dialog' });
294
+ button = screen.getByRole('button', { name: 'close' });
289
295
  await user.click(button);
290
296
 
291
297
  expect(mockOnClose).toHaveBeenCalledTimes(1);
@@ -300,6 +306,7 @@ describe('<AbstractContractFrame />', () => {
300
306
  const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
301
307
  const mockOnDone = jest.fn();
302
308
  const mockOnClose = jest.fn();
309
+ fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
303
310
 
304
311
  await act(async () => {
305
312
  render(
@@ -323,7 +330,7 @@ describe('<AbstractContractFrame />', () => {
323
330
  );
324
331
 
325
332
  // Dummy signature interface should have been rendered
326
- const button = screen.getByRole('button', { name: 'Close dialog' });
333
+ const button = screen.getByRole('button', { name: 'close' });
327
334
  await user.click(button);
328
335
 
329
336
  expect(mockOnClose).toHaveBeenCalledTimes(1);
@@ -1,11 +1,11 @@
1
1
  import React, { lazy, Suspense, useEffect, useRef, useState } from 'react';
2
- import { Button, Loader } from '@openfun/cunningham-react';
2
+ import { Button, Loader, Modal, ModalSize } from '@openfun/cunningham-react';
3
3
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
4
- import { Modal } from 'components/Modal';
5
4
  import { Maybe } from 'types/utils';
6
5
  import { CONTRACT_SETTINGS } from 'settings';
7
6
  import Banner, { BannerType } from 'components/Banner';
8
7
  import { SuccessIcon } from 'components/SuccessIcon';
8
+ import { noop } from 'utils';
9
9
 
10
10
  /*
11
11
  /!\ This component should not be used directly, only its implementations should be.
@@ -63,7 +63,7 @@ export const messages = defineMessages({
63
63
  },
64
64
  finishedDescription: {
65
65
  defaultMessage:
66
- 'You will receive an email once your contract will be fully signed. You can now enroll in your course runs!',
66
+ 'You will receive an email once your contract will be fully signed. You can now finalize your subscription.',
67
67
  description: 'Message displayed inside the contract signin modal when the contract is signed.',
68
68
  id: 'components.DashboardItem.Order.ContractFrame.finishedDescription',
69
69
  },
@@ -109,15 +109,16 @@ export interface SignatureProps {
109
109
  invitationLink: string;
110
110
  }
111
111
 
112
- const AbstractContractFrame = ({ isOpen, ...props }: Props) => {
112
+ const AbstractContractFrame = ({ isOpen, onClose = noop, ...props }: Props) => {
113
113
  return (
114
114
  <Modal
115
115
  isOpen={isOpen}
116
- shouldCloseOnOverlayClick={false}
117
- shouldCloseOnEsc={false}
118
- onRequestClose={props.onClose}
116
+ closeOnClickOutside={false}
117
+ closeOnEsc={false}
118
+ onClose={onClose}
119
+ size={ModalSize.LARGE}
119
120
  >
120
- <ContractFrameContent {...props} />
121
+ <ContractFrameContent {...props} onClose={onClose} />
121
122
  </Modal>
122
123
  );
123
124
  };
@@ -220,11 +221,13 @@ const ContractFrameContent = ({
220
221
 
221
222
  const renderLoadingContract = () => {
222
223
  return (
223
- <div className="ContractFrame__loading-container">
224
+ <div className="ContractFrame__container">
224
225
  <h3 className="ContractFrame__caption">
225
226
  <FormattedMessage {...messages.loadingContract} />
226
227
  </h3>
227
- <Loader />
228
+ <div className="ContractFrame__footer">
229
+ <Loader />
230
+ </div>
228
231
  </div>
229
232
  );
230
233
  };
@@ -256,20 +259,20 @@ const ContractFrameContent = ({
256
259
  </Suspense>
257
260
  )}
258
261
  {step === ContractSteps.POLLING && (
259
- <div className="ContractFrame__loading-container">
260
- <div>
261
- <h3 className="ContractFrame__caption">
262
- <FormattedMessage {...messages.polling} />
263
- </h3>
264
- <p className="ContractFrame__content">
265
- <FormattedMessage {...messages.pollingDescription} />
266
- </p>
262
+ <div className="ContractFrame__container">
263
+ <h3 className="ContractFrame__caption">
264
+ <FormattedMessage {...messages.polling} />
265
+ </h3>
266
+ <p className="ContractFrame__content">
267
+ <FormattedMessage {...messages.pollingDescription} />
268
+ </p>
269
+ <div className="ContractFrame__footer">
270
+ <Loader />
267
271
  </div>
268
- <Loader />
269
272
  </div>
270
273
  )}
271
274
  {step === ContractSteps.FINISHED && (
272
- <div className="ContractFrame__finished">
275
+ <div className="ContractFrame__container">
273
276
  <SuccessIcon />
274
277
  <h3 className="ContractFrame__caption">
275
278
  <FormattedMessage {...messages.finishedCaption} />
@@ -277,9 +280,11 @@ const ContractFrameContent = ({
277
280
  <p className="ContractFrame__content">
278
281
  <FormattedMessage {...messages.finishedDescription} />
279
282
  </p>
280
- <Button onClick={onClose}>
281
- <FormattedMessage {...messages.finishedButton} />
282
- </Button>
283
+ <div className="ContractFrame__footer">
284
+ <Button onClick={onClose}>
285
+ <FormattedMessage {...messages.finishedButton} />
286
+ </Button>
287
+ </div>
283
288
  </div>
284
289
  )}
285
290
  </div>
@@ -1,12 +1,12 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
- import { CredentialOrder, NestedCredentialOrder } from 'types/Joanie';
2
+ import { Order, AbstractNestedOrder } from 'types/Joanie';
3
3
  import { useJoanieApi } from 'contexts/JoanieApiContext';
4
4
  import AbstractContractFrame, {
5
5
  AbstractProps,
6
6
  } from 'components/ContractFrame/AbstractContractFrame';
7
7
 
8
8
  interface Props extends AbstractProps {
9
- order: CredentialOrder | NestedCredentialOrder;
9
+ order: Order | AbstractNestedOrder;
10
10
  }
11
11
 
12
12
  const LearnerContractFrame = ({ order, onDone, ...props }: Props) => {
@@ -17,15 +17,13 @@ iframe#lex-persona {
17
17
 
18
18
  .ContractFrame {
19
19
  &__modal-body {
20
- padding: 0 3rem 1.5rem 3rem;
20
+ padding: 1.5rem 3rem;
21
21
  }
22
22
 
23
- &__loading-container {
23
+ &__container {
24
24
  display: flex;
25
25
  flex-direction: column;
26
26
  align-items: center;
27
- padding: 3rem 0;
28
- gap: 4rem;
29
27
  }
30
28
 
31
29
  &__caption {
@@ -33,16 +31,6 @@ iframe#lex-persona {
33
31
  font-size: 1.5rem;
34
32
  }
35
33
 
36
- &__finished {
37
- display: flex;
38
- flex-direction: column;
39
- align-items: center;
40
-
41
- button {
42
- margin-top: 2.5rem;
43
- }
44
- }
45
-
46
34
  &__content {
47
35
  color: r-theme-val(contract-frame, content-color);
48
36
  font-size: 0.875rem;
@@ -59,4 +47,8 @@ iframe#lex-persona {
59
47
  align-items: center;
60
48
  gap: 2rem;
61
49
  }
50
+
51
+ &__footer {
52
+ margin-top: 2.5rem;
53
+ }
62
54
  }
@@ -1,21 +1,16 @@
1
1
  import { screen, within } from '@testing-library/react';
2
- import { useMemo, useState } from 'react';
2
+ import { useState, useEffect } from 'react';
3
3
  import fetchMock from 'fetch-mock';
4
4
  import { faker } from '@faker-js/faker';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
- import { CreditCardSelector } from 'components/SaleTunnel/CreditCardSelector/index';
8
7
  import { render } from 'utils/test/render';
9
8
  import { CreditCard } from 'types/Joanie';
10
- import {
11
- CredentialOrderFactory,
12
- CreditCardFactory,
13
- ProductFactory,
14
- } from 'utils/test/factories/joanie';
15
- import { SaleTunnelProps } from 'components/SaleTunnel/index';
9
+ import { CreditCardFactory } from 'utils/test/factories/joanie';
16
10
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
17
11
  import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
18
- import { SaleTunnelStep, SaleTunnelContext, SaleTunnelContextType } from '../GenericSaleTunnel';
12
+ import { SaleTunnelContextType } from 'components/SaleTunnel/GenericSaleTunnel';
13
+ import { CreditCardSelector } from 'components/CreditCardSelector/index';
19
14
 
20
15
  jest.mock('utils/context', () => ({
21
16
  __esModule: true,
@@ -43,30 +38,10 @@ describe('CreditCardSelector', () => {
43
38
 
44
39
  const Wrapper = () => {
45
40
  const [creditCard, setCreditCard] = useState<CreditCard>();
46
- const context: SaleTunnelContextType = useMemo(
47
- () => ({
48
- webAnalyticsEventKey: 'eventKey',
49
- order: CredentialOrderFactory().one(),
50
- product: ProductFactory().one(),
51
- props: {} as SaleTunnelProps,
52
- setBillingAddress: jest.fn(),
53
- creditCard,
54
- setCreditCard,
55
- onPaymentSuccess: jest.fn(),
56
- step: SaleTunnelStep.PAYMENT,
57
- registerSubmitCallback: jest.fn(),
58
- unregisterSubmitCallback: jest.fn(),
59
- runSubmitCallbacks: jest.fn(),
60
- }),
61
- [creditCard],
62
- );
63
- contextRef.current = context;
64
-
65
- return (
66
- <SaleTunnelContext.Provider value={context}>
67
- <CreditCardSelector />
68
- </SaleTunnelContext.Provider>
69
- );
41
+ useEffect(() => {
42
+ contextRef.current.creditCard = creditCard;
43
+ }, [creditCard]);
44
+ return <CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />;
70
45
  };
71
46
 
72
47
  return { contextRef, Wrapper };
@@ -77,17 +52,12 @@ describe('CreditCardSelector', () => {
77
52
  const { Wrapper } = buildWrapper();
78
53
  render(<Wrapper />);
79
54
 
80
- screen.getByRole('heading', {
81
- name: 'Payment method',
82
- });
83
- screen.getByText('Choose your payment method or add a new one during the payment.');
84
-
85
55
  // During loading state, the spinner should be displayed and the current selected card should not be displayed.
86
- expect(screen.queryByText('Add new credit card during payment')).not.toBeInTheDocument();
56
+ expect(screen.queryByText('Use another credit card')).not.toBeInTheDocument();
87
57
  await expectSpinner();
88
58
  await expectNoSpinner();
89
59
 
90
- screen.getByText('Use another credit card during payment');
60
+ screen.getByText('Use another credit card');
91
61
 
92
62
  // As the user has no credit card, the edit button should not be displayed.
93
63
  expect(
@@ -236,7 +206,7 @@ describe('CreditCardSelector', () => {
236
206
  await user.click(editButton);
237
207
 
238
208
  const radio = screen.getByRole('radio', {
239
- name: /Use another credit card during payment/i,
209
+ name: /Use another credit card/i,
240
210
  });
241
211
  await user.click(radio);
242
212
 
@@ -246,7 +216,7 @@ describe('CreditCardSelector', () => {
246
216
  await user.click(submitButton);
247
217
 
248
218
  expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
249
- screen.getByText('Use another credit card during payment');
219
+ screen.getByText('Use another credit card');
250
220
  expect(contextRef.current.creditCard).toBeUndefined();
251
221
  });
252
222
 
@@ -274,15 +244,15 @@ describe('CreditCardSelector', () => {
274
244
  await screen.findByTestId('credit-card-' + mainCreditCard.id);
275
245
  screen.getByText(mainCreditCard.title!);
276
246
  screen.getByText('Ends with •••• ' + mainCreditCard.last_numbers);
277
- expect(screen.queryByText('Add new credit card during payment')).not.toBeInTheDocument();
278
247
  expect(contextRef.current.creditCard!.id).toEqual(mainCreditCard.id);
279
248
 
280
249
  const user = userEvent.setup();
281
- const button = screen.getByRole('button', { name: /use another credit card during payment/i });
250
+ const button = screen.getByRole('button', { name: /use another credit card/i });
282
251
  await user.click(button);
283
252
 
284
253
  expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
285
- screen.getByText('Use another credit card during payment');
254
+ screen.getByText('Use another credit card');
255
+ expect(button).not.toBeInTheDocument();
286
256
  expect(contextRef.current.creditCard).toBeUndefined();
287
257
  });
288
258
 
@@ -13,7 +13,6 @@ import { CreditCardBrandLogo } from 'pages/DashboardCreditCardsManagement/Credit
13
13
  import { CreditCard } from 'types/Joanie';
14
14
  import { useCreditCardsManagement } from 'hooks/useCreditCardsManagement';
15
15
  import { Spinner } from 'components/Spinner';
16
- import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
17
16
  import { CreditCardExpirationStatus, CreditCardHelper } from 'utils/CreditCardHelper';
18
17
  import { useMatchMediaLg } from 'hooks/useMatchMedia';
19
18
 
@@ -33,20 +32,10 @@ const messages = defineMessages({
33
32
  description: 'Text to show the credit card expired date',
34
33
  defaultMessage: 'Expired since {month}/{year}',
35
34
  },
36
- title: {
37
- id: 'components.SaleTunnel.CreditCardSelector.title',
38
- description: 'Title for the credit card section',
39
- defaultMessage: 'Payment method',
40
- },
41
- description: {
42
- id: 'components.SaleTunnel.CreditCardSelector.description',
43
- description: 'Description for the credit card section',
44
- defaultMessage: 'Choose your payment method or add a new one during the payment.',
45
- },
46
35
  creditCardEmptyInlineDescription: {
47
36
  id: 'components.SaleTunnel.CreditCardSelector.creditCardEmptyInlineDescription',
48
37
  description: 'Description for the empty credit card inline',
49
- defaultMessage: 'Use another credit card during payment',
38
+ defaultMessage: 'Use another credit card',
50
39
  },
51
40
  modalTitle: {
52
41
  id: 'components.SaleTunnel.CreditCardSelector.modalTitle',
@@ -71,7 +60,19 @@ const messages = defineMessages({
71
60
  },
72
61
  });
73
62
 
74
- export const CreditCardSelector = () => {
63
+ export interface CreditCardSelectorProps {
64
+ creditCard?: CreditCard;
65
+ setCreditCard: (creditCard?: CreditCard) => void;
66
+ quickRemove?: boolean;
67
+ allowEdit?: boolean;
68
+ }
69
+
70
+ export const CreditCardSelector = ({
71
+ creditCard,
72
+ setCreditCard,
73
+ allowEdit = true,
74
+ quickRemove = true,
75
+ }: CreditCardSelectorProps) => {
75
76
  const intl = useIntl();
76
77
  const modal = useModal();
77
78
  const isMobile = useMatchMediaLg();
@@ -81,8 +82,6 @@ export const CreditCardSelector = () => {
81
82
  items: creditCards,
82
83
  } = useCreditCardsManagement();
83
84
 
84
- const { creditCard, setCreditCard } = useSaleTunnelContext();
85
-
86
85
  const getDefaultCreditCard = () => {
87
86
  if (creditCards.length === 0) {
88
87
  return;
@@ -102,12 +101,6 @@ export const CreditCardSelector = () => {
102
101
 
103
102
  return (
104
103
  <div className="credit-card-selector">
105
- <h4 className="block-title mb-t">
106
- <FormattedMessage {...messages.title} />
107
- </h4>
108
- <div className="description mb-s">
109
- <FormattedMessage {...messages.description} />
110
- </div>
111
104
  {fetching ? (
112
105
  <Spinner />
113
106
  ) : (
@@ -115,7 +108,7 @@ export const CreditCardSelector = () => {
115
108
  <div className="credit-card-selector__content">
116
109
  {creditCard ? <CreditCardInline creditCard={creditCard} /> : <CreditCardEmptyInline />}
117
110
 
118
- {creditCards?.length > 0 && (
111
+ {allowEdit && creditCards?.length > 0 && (
119
112
  <Button
120
113
  icon={<span className="material-icons">edit</span>}
121
114
  color="tertiary-text"
@@ -125,11 +118,11 @@ export const CreditCardSelector = () => {
125
118
  />
126
119
  )}
127
120
  </div>
128
- {creditCard && (
121
+ {creditCard && quickRemove && (
129
122
  <Button
130
123
  onClick={() => setCreditCard(undefined)}
131
124
  size="small"
132
- color="tertiary"
125
+ color="secondary"
133
126
  className="mt-t"
134
127
  fullWidth={isMobile}
135
128
  >
@@ -5,8 +5,8 @@ import userEvent from '@testing-library/user-event';
5
5
  import { QueryClientProvider } from '@tanstack/react-query';
6
6
  import fetchMock from 'fetch-mock';
7
7
  import { faker } from '@faker-js/faker';
8
- import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
9
8
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
+ import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
10
10
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
11
11
  import JoanieApiProvider from 'contexts/JoanieApiContext';
12
12
  import { alert } from 'utils/indirection/window';