richie-education 3.3.2-dev6 → 3.4.1-dev13

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 (179) hide show
  1. package/.eslintrc.json +0 -3
  2. package/.storybook/main.js +11 -12
  3. package/.storybook/preview.tsx +49 -24
  4. package/cunningham.cjs +31 -0
  5. package/i18n/compile-translations.js +12 -10
  6. package/i18n/locales/ar-SA.json +20 -0
  7. package/i18n/locales/es-ES.json +20 -0
  8. package/i18n/locales/fa-IR.json +20 -0
  9. package/i18n/locales/fr-CA.json +20 -0
  10. package/i18n/locales/fr-FR.json +21 -1
  11. package/i18n/locales/ko-KR.json +20 -0
  12. package/i18n/locales/pt-PT.json +42 -22
  13. package/i18n/locales/ru-RU.json +20 -0
  14. package/i18n/locales/vi-VN.json +20 -0
  15. package/jest.config.js +5 -0
  16. package/js/api/joanie.ts +20 -0
  17. package/js/api/utils.ts +4 -3
  18. package/js/components/AddressesManagement/AddressForm/index.stories.tsx +1 -1
  19. package/js/components/AddressesManagement/AddressForm/index.tsx +4 -3
  20. package/js/components/AddressesManagement/index.stories.tsx +1 -1
  21. package/js/components/AddressesManagement/index.tsx +5 -3
  22. package/js/components/Badge/index.stories.tsx +1 -1
  23. package/js/components/Badge/index.tsx +1 -1
  24. package/js/components/Banner/index.stories.tsx +1 -1
  25. package/js/components/CourseGlimpse/index.stories.tsx +1 -1
  26. package/js/components/CourseGlimpseList/index.stories.tsx +1 -1
  27. package/js/components/CreditCardSelector/_styles.scss +2 -2
  28. package/js/components/CreditCardSelector/index.tsx +11 -3
  29. package/js/components/DownloadAgreementButton/index.tsx +51 -0
  30. package/js/components/DownloadBatchOrderSeatsButton/index.spec.tsx +46 -0
  31. package/js/components/DownloadBatchOrderSeatsButton/index.tsx +80 -0
  32. package/js/components/DownloadCertificateButton/index.tsx +2 -1
  33. package/js/components/DownloadContractButton/index.tsx +7 -1
  34. package/js/components/Form/Form/index.tsx +4 -2
  35. package/js/components/Icon/index.stories.tsx +2 -1
  36. package/js/components/Modal/index.stories.tsx +1 -1
  37. package/js/components/Modal/index.tsx +2 -1
  38. package/js/components/OpenEdxFullNameForm/index.stories.tsx +1 -1
  39. package/js/components/OpenEdxFullNameForm/index.tsx +2 -2
  40. package/js/components/PaymentScheduleGrid/_styles.scss +2 -2
  41. package/js/components/PurchaseButton/index.stories.tsx +1 -1
  42. package/js/components/RegisteredAddress/index.stories.tsx +1 -1
  43. package/js/components/RegisteredAddress/index.tsx +4 -2
  44. package/js/components/SaleTunnel/AddressSelector/CreateAddressFormModal.tsx +1 -1
  45. package/js/components/SaleTunnel/AddressSelector/EditAddressFormModal.tsx +1 -1
  46. package/js/components/SaleTunnel/AddressSelector/index.tsx +4 -2
  47. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +5 -4
  48. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +27 -5
  49. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +1 -1
  50. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +9 -6
  51. package/js/components/SaleTunnel/_styles.scss +9 -8
  52. package/js/components/SaleTunnel/index.credential.spec.tsx +50 -1
  53. package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -5
  54. package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +1 -1
  55. package/js/components/SaleTunnel/index.stories.tsx +1 -1
  56. package/js/components/Spinner/index.stories.tsx +1 -1
  57. package/js/components/Tabs/index.stories.tsx +1 -1
  58. package/js/components/Tabs/index.tsx +2 -1
  59. package/js/components/TeacherDashboardCourseList/index.tsx +2 -1
  60. package/js/hooks/useAddressesManagement.tsx +4 -2
  61. package/js/hooks/useBatchOrder/index.tsx +21 -1
  62. package/js/hooks/useCreditCards/index.ts +6 -4
  63. package/js/hooks/useDashboardAddressForm.tsx +3 -3
  64. package/js/hooks/useDownloadAgreement/index.spec.tsx +136 -0
  65. package/js/hooks/useDownloadAgreement/index.tsx +25 -0
  66. package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +132 -0
  67. package/js/hooks/useDownloadBatchOrderSeats/index.tsx +24 -0
  68. package/js/hooks/useMatchMedia.ts +1 -1
  69. package/js/hooks/useResources/useResourcesRoot.ts +1 -1
  70. package/js/hooks/useUnionResource/index.ts +6 -2
  71. package/js/hooks/useUnionResource/utils/fetchEntity.ts +1 -0
  72. package/js/pages/DashboardAddressesManagement/DashboardAddressBox.tsx +3 -3
  73. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.stories.tsx +1 -1
  74. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.tsx +1 -1
  75. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.stories.tsx +1 -1
  76. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.tsx +3 -2
  77. package/js/pages/DashboardAddressesManagement/index.stories.tsx +1 -1
  78. package/js/pages/DashboardAddressesManagement/index.tsx +1 -1
  79. package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +19 -2
  80. package/js/pages/DashboardCourses/index.tsx +2 -1
  81. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.stories.tsx +1 -1
  82. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -3
  83. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.stories.tsx +1 -1
  84. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +3 -2
  85. package/js/pages/DashboardCreditCardsManagement/index.stories.tsx +1 -1
  86. package/js/pages/DashboardOpenEdxProfile/index.stories.tsx +1 -1
  87. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -2
  88. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +2 -1
  89. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -4
  90. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +8 -9
  91. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +14 -3
  92. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +6 -1
  93. package/js/pages/TeacherDashboardCourseLoader/CourseRunList/utils.tsx +2 -1
  94. package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +4 -2
  95. package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +2 -1
  96. package/js/pages/TeacherDashboardOrganizationQuotes/BatchOrderSeatInfoQuote.tsx +112 -0
  97. package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +17 -0
  98. package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +7 -4
  99. package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +8 -4
  100. package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +39 -26
  101. package/js/translations/ar-SA.json +1 -1
  102. package/js/translations/es-ES.json +1 -1
  103. package/js/translations/fa-IR.json +1 -1
  104. package/js/translations/fr-CA.json +1 -1
  105. package/js/translations/fr-FR.json +1 -1
  106. package/js/translations/ko-KR.json +1 -1
  107. package/js/translations/pt-PT.json +1 -1
  108. package/js/translations/ru-RU.json +1 -1
  109. package/js/translations/vi-VN.json +1 -1
  110. package/js/types/Joanie.ts +22 -2
  111. package/js/utils/ProductHelper/index.spec.ts +1 -1
  112. package/js/utils/StorybookHelper/index.tsx +3 -6
  113. package/js/utils/cunningham-tokens.ts +1111 -142
  114. package/js/utils/download.ts +3 -1
  115. package/js/utils/errors/handle.spec.ts +3 -3
  116. package/js/utils/react-query/useSessionMutation/index.ts +8 -3
  117. package/js/utils/test/factories/joanie.ts +16 -2
  118. package/js/widgets/Dashboard/components/DashboardAvatar/index.stories.tsx +1 -1
  119. package/js/widgets/Dashboard/components/DashboardBox/index.stories.tsx +13 -5
  120. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.stories.tsx +1 -1
  121. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.tsx +3 -2
  122. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +13 -2
  123. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +12 -4
  124. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +1 -1
  125. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderAgreementInfo.tsx +72 -0
  126. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +1 -1
  127. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +2 -1
  128. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.spec.tsx +114 -0
  129. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +133 -0
  130. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +17 -1
  131. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts +24 -0
  132. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +16 -3
  133. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.stories.tsx +1 -1
  134. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.stories.tsx +1 -1
  135. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.stories.tsx +1 -1
  136. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -4
  137. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.stories.tsx +1 -1
  138. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +1 -1
  139. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderReadonly.stories.tsx +1 -1
  140. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderWritable.stories.tsx +1 -1
  141. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +1 -1
  142. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +2 -2
  143. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -1
  144. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +6 -3
  145. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +6 -2
  146. package/js/widgets/Dashboard/components/DashboardItem/index.stories.tsx +1 -1
  147. package/js/widgets/Dashboard/components/DashboardItem/index.tsx +2 -1
  148. package/js/widgets/Dashboard/components/DashboardListAvatar/index.stories.tsx +1 -1
  149. package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +1 -1
  150. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.stories.tsx +1 -1
  151. package/js/widgets/Dashboard/components/ProtectedOutlet/AuthenticatedOutlet.spec.tsx +1 -1
  152. package/js/widgets/Dashboard/components/ProtectedOutlet/ProtectedOutlet.spec.tsx +1 -1
  153. package/js/widgets/Dashboard/components/SearchBar/index.tsx +2 -1
  154. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.stories.tsx +1 -1
  155. package/js/widgets/Dashboard/components/TeacherDashboardProfileSidebar/index.stories.tsx +1 -1
  156. package/js/widgets/Dashboard/hooks/useRouteInfo/index.spec.tsx +2 -2
  157. package/js/widgets/Dashboard/index.spec.tsx +1 -1
  158. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +2 -2
  159. package/js/widgets/Search/components/SearchFilterValueParent/index.stories.tsx +1 -1
  160. package/js/widgets/Search/components/SearchFiltersPane/index.tsx +2 -1
  161. package/js/widgets/Slider/index.stories.tsx +1 -1
  162. package/js/widgets/Slider/index.tsx +7 -6
  163. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.stories.tsx +1 -1
  164. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.stories.tsx +1 -1
  165. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +1 -1
  166. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +4 -2
  167. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +1 -1
  168. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.stories.tsx +41 -0
  169. package/js/widgets/UserLogin/index.stories.tsx +1 -1
  170. package/package.json +76 -81
  171. package/scss/components/_subheader.scss +1 -1
  172. package/scss/components/templates/richie/slider/_slider.scss +1 -1
  173. package/scss/objects/_course_glimpses.scss +1 -0
  174. package/scss/objects/_dashboard.scss +77 -0
  175. package/scss/trumps/_bootstrap.scss +1 -0
  176. package/scss/vendors/css/cunningham-tokens.css +1259 -154
  177. package/scss/vendors/cunningham-tokens.scss +1479 -150
  178. package/tsconfig.json +1 -1
  179. package/webpack.config.js +8 -0
@@ -75,7 +75,8 @@ export const AddressSelector = () => {
75
75
  <Button
76
76
  size="small"
77
77
  icon={<span className="material-icons">edit</span>}
78
- color="tertiary"
78
+ color="brand"
79
+ variant="tertiary"
79
80
  onClick={editFormModal.open}
80
81
  fullWidth={isMobile}
81
82
  >
@@ -85,7 +86,8 @@ export const AddressSelector = () => {
85
86
  <Button
86
87
  size="small"
87
88
  icon={<span className="material-icons">add</span>}
88
- color="primary"
89
+ color="brand"
90
+ variant="primary"
89
91
  onClick={createFormModal.open}
90
92
  fullWidth={isMobile}
91
93
  >
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
- import { FormProvider, useForm } from 'react-hook-form';
3
+ import { FormProvider, Resolver, useForm } from 'react-hook-form';
4
4
  import { yupResolver } from '@hookform/resolvers/yup';
5
5
  import * as Yup from 'yup';
6
6
  import { Step, StepLabel, Stepper } from '@mui/material';
@@ -96,7 +96,7 @@ export const SaleTunnelInformationGroup = () => {
96
96
  );
97
97
  };
98
98
 
99
- const EMAIL_REGEX = /^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$/;
99
+ const EMAIL_REGEX = /^[\w.+\-]+@([\w-]+\.)+[\w-]{2,4}$/;
100
100
 
101
101
  export const validationSchema = Yup.object().shape({
102
102
  offering_id: Yup.string().required(),
@@ -197,7 +197,7 @@ const BatchOrderForm = () => {
197
197
  const form = useForm<BatchOrder>({
198
198
  defaultValues: batchOrder || defaultValues,
199
199
  mode: 'onBlur',
200
- resolver: yupResolver(validationSchema),
200
+ resolver: yupResolver(validationSchema) as Resolver<BatchOrder>,
201
201
  });
202
202
  const { watch } = form;
203
203
  const values = watch();
@@ -268,7 +268,8 @@ const BatchOrderForm = () => {
268
268
  }
269
269
  }}
270
270
  hidden={activeStep === 0}
271
- color="tertiary"
271
+ color="brand"
272
+ variant="tertiary"
272
273
  >
273
274
  <FormattedMessage {...messages.previousButton} />
274
275
  </Button>
@@ -143,7 +143,7 @@ const messages = defineMessages({
143
143
  id: 'components.SaleTunnel.Information.cpf.description',
144
144
  description: 'Explanatory text for the CPF payment option',
145
145
  defaultMessage:
146
- 'Pay for your training using your personal training account (CPF) on Mon Compte Formation.',
146
+ 'Purchase your training course by using your Personal Training Account (CPF) on Mon Compte Formation.',
147
147
  },
148
148
  cpfButtonLabel: {
149
149
  id: 'components.SaleTunnel.Information.cpf.buttonLabel',
@@ -236,7 +236,12 @@ export const SaleTunnelInformationSingular = () => {
236
236
  </div>
237
237
  )}
238
238
  {paymentMode === PaymentMode.CPF ? (
239
- <CpfPayment deepLink={deepLink!} />
239
+ <CpfPayment
240
+ deepLink={deepLink!}
241
+ discount={discount}
242
+ voucherError={voucherError}
243
+ setVoucherError={setVoucherError}
244
+ />
240
245
  ) : (
241
246
  <>
242
247
  {needsPayment && (
@@ -443,7 +448,13 @@ const Voucher = ({
443
448
  label={intl.formatMessage(messages.voucherTitle)}
444
449
  disabled={!!voucherCode}
445
450
  />
446
- <Button size="small" color="primary" onClick={submitVoucher} disabled={!!voucherCode}>
451
+ <Button
452
+ size="small"
453
+ color="brand"
454
+ variant="primary"
455
+ onClick={submitVoucher}
456
+ disabled={!!voucherCode}
457
+ >
447
458
  <FormattedMessage {...messages.voucherValidate} />
448
459
  </Button>
449
460
  </div>
@@ -493,14 +504,24 @@ const PaymentScheduleBlock = ({ schedule }: { schedule: PaymentSchedule }) => {
493
504
  );
494
505
  };
495
506
 
496
- const CpfPayment = ({ deepLink }: { deepLink: string }) => {
507
+ const CpfPayment = ({
508
+ deepLink,
509
+ discount,
510
+ voucherError,
511
+ setVoucherError,
512
+ }: {
513
+ deepLink: string;
514
+ discount?: string;
515
+ voucherError: HttpError | null;
516
+ setVoucherError: (value: HttpError | null) => void;
517
+ }) => {
497
518
  return (
498
519
  <div className="sale-tunnel__cpf">
499
520
  <p className="description mb-s">
500
521
  <FormattedMessage {...messages.cpfDescription} />
501
522
  </p>
502
523
  <Button
503
- color="primary"
524
+ color="brand"
504
525
  fullWidth={true}
505
526
  href={deepLink}
506
527
  target="_blank"
@@ -508,6 +529,7 @@ const CpfPayment = ({ deepLink }: { deepLink: string }) => {
508
529
  >
509
530
  <FormattedMessage {...messages.cpfButtonLabel} />
510
531
  </Button>
532
+ <Voucher discount={discount} voucherError={voucherError} setVoucherError={setVoucherError} />
511
533
  </div>
512
534
  );
513
535
  };
@@ -20,7 +20,7 @@ const messages = defineMessages({
20
20
  },
21
21
  successDetailMessage: {
22
22
  defaultMessage:
23
- 'You will be able to start your training once the first installment will be paid.',
23
+ 'You’ll be able to start your training once the first installment has been paid, or once access opens if no payment is required.',
24
24
  description: 'Text to explain when the user will be able to start its training.',
25
25
  id: 'components.SaleTunnelSuccess.successDetailMessage',
26
26
  },
@@ -110,9 +110,6 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
110
110
  paymentMode,
111
111
  } = useSaleTunnelContext();
112
112
 
113
- if (paymentMode === PaymentMode.CPF) {
114
- return null;
115
- }
116
113
  const { methods: orderMethods } = useOrders(undefined, { enabled: false });
117
114
  const { methods: batchOrderMethods } = useBatchOrder();
118
115
  const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
@@ -138,12 +135,17 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
138
135
  return;
139
136
  }
140
137
 
141
- if (!billingAddress && needsPayment) {
138
+ if (!billingAddress && needsPayment && paymentMode !== PaymentMode.CPF) {
142
139
  handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
143
140
  return;
144
141
  }
145
142
 
146
- if (!saleTunnelProps.isWithdrawable && !hasWaivedWithdrawalRight && needsPayment) {
143
+ if (
144
+ !saleTunnelProps.isWithdrawable &&
145
+ !hasWaivedWithdrawalRight &&
146
+ needsPayment &&
147
+ paymentMode !== PaymentMode.CPF
148
+ ) {
147
149
  handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
148
150
  return;
149
151
  }
@@ -201,6 +203,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
201
203
  };
202
204
 
203
205
  const walkthroughMessages = useMemo(() => {
206
+ if (batchOrder) return;
204
207
  if (product.contract_definition && product.price > 0) {
205
208
  return messages.walkthroughToSignAndSavePayment;
206
209
  } else if (product.contract_definition && product.price === 0) {
@@ -208,7 +211,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
208
211
  } else if (!product.contract_definition && product.price > 0 && needsPayment) {
209
212
  return messages.walkthroughToSavePayment;
210
213
  }
211
- }, [product, creditCard, needsPayment]);
214
+ }, [product, creditCard, needsPayment, batchOrder]);
212
215
 
213
216
  useEffect(() => {
214
217
  if (order) nextStep();
@@ -32,12 +32,13 @@
32
32
  &__right {
33
33
  flex: 1;
34
34
  overflow: hidden;
35
+ position: relative;
35
36
  }
36
37
 
37
38
  &__column {
38
39
  display: flex;
39
40
  flex-direction: column;
40
- gap: var(--c--theme--spacings--b);
41
+ gap: var(--c--globals--spacings--b);
41
42
  }
42
43
  }
43
44
 
@@ -135,7 +136,7 @@
135
136
  margin-top: 1rem;
136
137
  display: flex;
137
138
  justify-content: space-between;
138
- font-size: var(--c--theme--font--sizes--l);
139
+ font-size: var(--c--globals--font--sizes--md);
139
140
  }
140
141
  }
141
142
 
@@ -159,8 +160,8 @@
159
160
  gap: 0.5rem;
160
161
 
161
162
  .title {
162
- font-weight: var(--c--theme--font--weights--bold);
163
- font-size: var(--c--theme--font--sizes--l);
163
+ font-weight: var(--c--globals--font--weights--bold);
164
+ font-size: var(--c--globals--font--sizes--md);
164
165
  margin: 0 0 0.5rem;
165
166
  }
166
167
 
@@ -209,17 +210,17 @@
209
210
  }
210
211
 
211
212
  .block-title {
212
- font-size: var(--c--theme--font--sizes--l);
213
+ font-size: var(--c--globals--font--sizes--md);
213
214
  color: r-theme-val(sale-tunnel, title-color);
214
- font-weight: var(--c--theme--font--weights--extrabold);
215
+ font-weight: var(--c--globals--font--weights--extrabold);
215
216
  text-align: left;
216
- font-family: var(--c--theme--font--families--accent);
217
+ font-family: var(--c--globals--font--families--accent);
217
218
  }
218
219
 
219
220
  .sub-block-title {
220
221
  font-size: 12px;
221
222
  color: r-theme-val(sale-tunnel, title-color);
222
- font-weight: var(--c--theme--font--weights--bold);
223
+ font-weight: var(--c--globals--font--weights--bold);
223
224
  text-align: left;
224
225
  }
225
226
 
@@ -166,7 +166,9 @@ describe('SaleTunnel / Credential', () => {
166
166
 
167
167
  // - CPF description and redirect button should be visible.
168
168
  expect(
169
- screen.getByText(/pay for your training using your personal training account/i),
169
+ screen.getByText(
170
+ /purchase your training course by using your Personal Training Account \(CPF\) on Mon Compte Formation/i,
171
+ ),
170
172
  ).toBeInTheDocument();
171
173
  const cpfButton = screen.getByRole('link', { name: /go to mon compte formation/i });
172
174
 
@@ -222,4 +224,51 @@ describe('SaleTunnel / Credential', () => {
222
224
  // - Classic billing information section should be displayed.
223
225
  expect(screen.getByText(/this information will be used for billing/i)).toBeInTheDocument();
224
226
  });
227
+
228
+ it('should display voucher input and subscribe button in CPF mode', async () => {
229
+ const course = PacedCourseFactory().one();
230
+ const product = CredentialProductFactory().one();
231
+ const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
232
+ const deepLink = 'https://placeholder.com/course/1';
233
+ const orderQueryParameters = {
234
+ course_code: course.code,
235
+ product_id: product.id,
236
+ state: NOT_CANCELED_ORDER_STATES,
237
+ };
238
+
239
+ fetchMock
240
+ .get(
241
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
242
+ [],
243
+ )
244
+ .get(
245
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
246
+ [],
247
+ )
248
+ .get(
249
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
250
+ { deep_link: deepLink },
251
+ )
252
+ .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
253
+ overwriteRoutes: true,
254
+ });
255
+
256
+ const user = userEvent.setup({ delay: null });
257
+
258
+ render(<Wrapper product={product} course={course} />, {
259
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
260
+ });
261
+
262
+ await screen.findByRole('heading', { level: 3, name: /payment method/i });
263
+
264
+ // Switch to CPF mode
265
+ await user.click(screen.getByRole('radio', { name: /my training account \(cpf\)/i }));
266
+
267
+ // - Voucher input should be visible in CPF mode.
268
+ expect(screen.getByLabelText('Voucher code')).toBeInTheDocument();
269
+ expect(screen.getByRole('button', { name: 'Validate' })).toBeInTheDocument();
270
+
271
+ // - Subscribe button should be visible in CPF mode.
272
+ expect(screen.getByRole('button', { name: 'Subscribe' })).toBeInTheDocument();
273
+ });
225
274
  });
@@ -210,11 +210,11 @@ describe('SaleTunnel', () => {
210
210
  await user.type($lastName, 'Doe');
211
211
  await user.type($firstName, 'John');
212
212
  await user.type($role, 'HR');
213
- await user.type($email, 'john.doe@fun-mooc.com');
213
+ await user.type($email, 'john.doe+admin@fun-mooc.com');
214
214
  await user.type($phone, '+338203920103');
215
215
 
216
216
  expect($lastName).toHaveValue('Doe');
217
- expect($email).toHaveValue('john.doe@fun-mooc.com');
217
+ expect($email).toHaveValue('john.doe+admin@fun-mooc.com');
218
218
 
219
219
  // Signatory step
220
220
  await user.click(screen.getByRole('button', { name: 'Next' }));
@@ -227,7 +227,7 @@ describe('SaleTunnel', () => {
227
227
  await user.type($signatoryLastName, 'Doe');
228
228
  await user.type($signatoryFirstName, 'John');
229
229
  await user.type($signatoryRole, 'CEO');
230
- await user.type($signatoryEmail, 'john.doe@fun-mooc.com');
230
+ await user.type($signatoryEmail, 'john.doe+ceo@fun-mooc.com');
231
231
  await user.type($signatoryPhone, '+338203920103');
232
232
 
233
233
  // Participants step
@@ -298,12 +298,12 @@ describe('SaleTunnel', () => {
298
298
  administrative_lastname: 'Doe',
299
299
  administrative_firstname: 'John',
300
300
  administrative_profession: 'HR',
301
- administrative_email: 'john.doe@fun-mooc.com',
301
+ administrative_email: 'john.doe+admin@fun-mooc.com',
302
302
  administrative_telephone: '+338203920103',
303
303
  signatory_lastname: 'Doe',
304
304
  signatory_firstname: 'John',
305
305
  signatory_profession: 'CEO',
306
- signatory_email: 'john.doe@fun-mooc.com',
306
+ signatory_email: 'john.doe+ceo@fun-mooc.com',
307
307
  signatory_telephone: '+338203920103',
308
308
  nb_seats: '13',
309
309
  payment_method: PaymentMethod.PURCHASE_ORDER,
@@ -404,7 +404,7 @@ describe('SaleTunnel', () => {
404
404
  await screen.findByTestId('generic-sale-tunnel-success-step');
405
405
  screen.getByText('Subscription confirmed!');
406
406
  screen.getByText(
407
- /your order has been successfully registered\.you will be able to start your training once the first installment will be paid\./i,
407
+ /you’ll be able to start your training once the first installment has been paid, or once access opens if no payment is required\./i,
408
408
  );
409
409
  screen.getByRole('link', { name: 'Close' });
410
410
 
@@ -1,4 +1,4 @@
1
- import { StoryObj, Meta } from '@storybook/react';
1
+ import { StoryObj, Meta } from '@storybook/react-webpack5';
2
2
  import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
3
3
  import {
4
4
  CertificateProductFactory,
@@ -1,4 +1,4 @@
1
- import { StoryObj, Meta } from '@storybook/react';
1
+ import { StoryObj, Meta } from '@storybook/react-webpack5';
2
2
  import { Spinner } from './index';
3
3
 
4
4
  export default {
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { useState } from 'react';
3
3
  import { RouterWrapper } from 'utils/test/wrappers/RouterWrapper';
4
4
  import Tabs from './index';
@@ -45,7 +45,8 @@ const Tab = ({
45
45
  return (
46
46
  <div className="tabs__tab">
47
47
  <Button
48
- color="tertiary-text"
48
+ color="brand"
49
+ variant="tertiary"
49
50
  onClick={handleOnClick}
50
51
  className={classNames('c__button--tab', {
51
52
  'c__button--active': isActive,
@@ -91,7 +91,8 @@ const TeacherDashboardCourseList = ({
91
91
  onClick={() => loadMore()}
92
92
  disabled={isLoadingMore}
93
93
  ref={loadMoreButtonRef}
94
- color="tertiary"
94
+ color="brand"
95
+ variant="tertiary"
95
96
  >
96
97
  <FormattedMessage {...messages.loadMore} />
97
98
  </Button>
@@ -1,5 +1,4 @@
1
1
  import { defineMessages, useIntl } from 'react-intl';
2
- import { MutateOptions } from '@tanstack/react-query';
3
2
  import { useAddresses } from 'hooks/useAddresses';
4
3
  import * as Joanie from 'types/Joanie';
5
4
  import { confirm } from 'utils/indirection/window';
@@ -58,7 +57,10 @@ export function useAddressesManagement() {
58
57
  * @param {Joanie.Address} address
59
58
  * @param {AddressesMutateOptions} options
60
59
  */
61
- const remove = (address: Joanie.Address, options?: MutateOptions) => {
60
+ const remove = (
61
+ address: Joanie.Address,
62
+ options?: Parameters<typeof addresses.methods.delete>[1],
63
+ ) => {
62
64
  if (address.is_main) {
63
65
  addresses.methods.setError(intl.formatMessage(messages.errorCannotRemoveMain));
64
66
  return;
@@ -1,7 +1,13 @@
1
1
  import { defineMessages } from 'react-intl';
2
2
  import { useJoanieApi } from 'contexts/JoanieApiContext';
3
3
  import { ResourcesQuery, useResource, useResources, UseResourcesProps } from 'hooks/useResources';
4
- import { API, BatchOrderQueryFilters, BatchOrderRead } from 'types/Joanie';
4
+ import {
5
+ API,
6
+ BatchOrderQueryFilters,
7
+ BatchOrderRead,
8
+ BatchOrderSeat,
9
+ BatchOrderSeatsQueryFilters,
10
+ } from 'types/Joanie';
5
11
 
6
12
  const messages = defineMessages({
7
13
  errorCreate: {
@@ -34,3 +40,17 @@ export const useBatchOrdersActions = () => {
34
40
  submitForPayment,
35
41
  };
36
42
  };
43
+
44
+ const seatsProps: UseResourcesProps<
45
+ BatchOrderSeat,
46
+ BatchOrderSeatsQueryFilters,
47
+ API['user']['batchOrders']['seats']
48
+ > = {
49
+ queryKey: ['batchOrders', 'seats'],
50
+ apiInterface: () => useJoanieApi().user.batchOrders.seats,
51
+ session: true,
52
+ };
53
+
54
+ export const useBatchOrderSeats = useResources<BatchOrderSeat, BatchOrderSeatsQueryFilters>(
55
+ seatsProps,
56
+ );
@@ -1,6 +1,5 @@
1
1
  import { defineMessages, useIntl } from 'react-intl';
2
2
  import { useMutation, useQueryClient } from '@tanstack/react-query';
3
- import { MutateOptions } from '@tanstack/query-core';
4
3
  import { API, CreditCard } from 'types/Joanie';
5
4
  import { useJoanieApi } from 'contexts/JoanieApiContext';
6
5
  import { useSessionMutation } from 'utils/react-query/useSessionMutation';
@@ -84,10 +83,13 @@ const useCreditCardResources =
84
83
  * If the error is a 409, it means the credit card is used to pay at least one order
85
84
  * and the user should be informed about that.
86
85
  */
87
- const deleteMutateAsync = async (creditCard: CreditCard, options?: MutateOptions) => {
86
+ const deleteMutateAsync = async (
87
+ creditCard: CreditCard,
88
+ options?: Parameters<typeof custom.methods.delete>[1],
89
+ ) => {
88
90
  return custom.methods.delete(creditCard.id, {
89
91
  ...options,
90
- onError: (error: HttpError, variables, context) => {
92
+ onError: (error: HttpError, variables, context, mutationContext) => {
91
93
  if (error.code === HttpStatusCode.CONFLICT) {
92
94
  custom.methods.setError(
93
95
  intl.formatMessage(messages.errorCannotDelete, {
@@ -97,7 +99,7 @@ const useCreditCardResources =
97
99
  } else {
98
100
  custom.methods.setError(intl.formatMessage(messages.errorDelete));
99
101
  }
100
- options?.onError?.(error, variables, context);
102
+ options?.onError?.(error, variables, context, mutationContext);
101
103
  },
102
104
  });
103
105
  };
@@ -1,5 +1,5 @@
1
- import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
2
- import { FormProvider, useForm } from 'react-hook-form';
1
+ import { yupResolver } from '@hookform/resolvers/yup';
2
+ import { FormProvider, Resolver, useForm } from 'react-hook-form';
3
3
  import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
4
4
  import * as Yup from 'yup';
5
5
  import countries from 'i18n-iso-countries';
@@ -60,7 +60,7 @@ export const useDashboardAddressForm = (address?: Address) => {
60
60
  defaultValues: address || defaultValues,
61
61
  mode: 'onBlur',
62
62
  reValidateMode: 'onChange',
63
- resolver: yupResolver(validationSchema),
63
+ resolver: yupResolver(validationSchema) as Resolver<AddressFormValues>,
64
64
  });
65
65
  const { register, handleSubmit, formState } = form;
66
66
 
@@ -0,0 +1,136 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import fetchMock from 'fetch-mock';
3
+ import { PropsWithChildren } from 'react';
4
+ import { QueryClientProvider } from '@tanstack/react-query';
5
+ import { IntlProvider } from 'react-intl';
6
+ import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
7
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
9
+ import { useDownloadAgreement } from 'hooks/useDownloadAgreement/index';
10
+ import { handle } from 'utils/errors/handle';
11
+ import { SessionProvider } from 'contexts/SessionContext';
12
+ import { Deferred } from 'utils/test/deferred';
13
+ import { OrganizationFactory } from 'utils/test/factories/joanie';
14
+ import { HttpStatusCode } from 'utils/errors/HttpError';
15
+
16
+ jest.mock('utils/errors/handle');
17
+ jest.mock('utils/context', () => ({
18
+ __esModule: true,
19
+ default: mockRichieContextFactory({
20
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
21
+ joanie_backend: { endpoint: 'https://joanie.test' },
22
+ }).one(),
23
+ }));
24
+
25
+ const mockHandle = handle as jest.MockedFn<typeof handle>;
26
+
27
+ describe('useDownloadAgreement', () => {
28
+ beforeEach(() => {
29
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
30
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
31
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
32
+ });
33
+
34
+ beforeAll(() => {
35
+ // eslint-disable-next-line compat/compat
36
+ URL.createObjectURL = jest.fn();
37
+ // eslint-disable-next-line compat/compat
38
+ URL.revokeObjectURL = jest.fn();
39
+ HTMLAnchorElement.prototype.click = jest.fn();
40
+ });
41
+
42
+ afterEach(() => {
43
+ jest.clearAllMocks();
44
+ fetchMock.restore();
45
+ });
46
+
47
+ const Wrapper = ({ children }: PropsWithChildren) => {
48
+ return (
49
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
50
+ <IntlProvider locale="en">
51
+ <SessionProvider>{children}</SessionProvider>
52
+ </IntlProvider>
53
+ </QueryClientProvider>
54
+ );
55
+ };
56
+
57
+ it('downloads the agreement PDF', async () => {
58
+ const organization = OrganizationFactory().one();
59
+ const agreementId = faker.string.uuid();
60
+ const DOWNLOAD_URL = `https://joanie.test/api/v1.0/organizations/${organization.id}/agreements/${agreementId}/download/`;
61
+ const deferred = new Deferred();
62
+ fetchMock.get(DOWNLOAD_URL, deferred.promise);
63
+
64
+ const { result } = renderHook(() => useDownloadAgreement(), {
65
+ wrapper: Wrapper,
66
+ });
67
+ await waitFor(() => expect(result.current).not.toBeNull());
68
+
69
+ expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
70
+ // eslint-disable-next-line compat/compat
71
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
72
+ // eslint-disable-next-line compat/compat
73
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
74
+ expect(result.current.loading).toBe(false);
75
+
76
+ act(() => {
77
+ result.current.download(organization.id, agreementId);
78
+ });
79
+ expect(result.current.loading).toBe(true);
80
+
81
+ deferred.resolve({
82
+ status: HttpStatusCode.OK,
83
+ body: new Blob(['%PDF-1.4']),
84
+ headers: {
85
+ 'Content-Disposition': 'attachment; filename="Convention_de_formation.pdf";',
86
+ 'Content-Type': 'application/pdf',
87
+ },
88
+ });
89
+
90
+ await waitFor(() => {
91
+ expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
92
+ // eslint-disable-next-line compat/compat
93
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
94
+ // eslint-disable-next-line compat/compat
95
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
96
+ expect(result.current.loading).toBe(false);
97
+ });
98
+
99
+ fireEvent.blur(window);
100
+ // eslint-disable-next-line compat/compat
101
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('handles an error if agreement download request fails', async () => {
105
+ const organization = OrganizationFactory().one();
106
+ const agreementId = faker.string.uuid();
107
+ const DOWNLOAD_URL = `https://joanie.test/api/v1.0/organizations/${organization.id}/agreements/${agreementId}/download/`;
108
+ fetchMock.get(DOWNLOAD_URL, HttpStatusCode.UNAUTHORIZED);
109
+
110
+ const { result } = renderHook(() => useDownloadAgreement(), {
111
+ wrapper: Wrapper,
112
+ });
113
+ await waitFor(() => expect(result.current).not.toBeNull());
114
+
115
+ expect(fetchMock.called(DOWNLOAD_URL)).toBe(false);
116
+ expect(mockHandle).not.toHaveBeenCalled();
117
+ // eslint-disable-next-line compat/compat
118
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
119
+ // eslint-disable-next-line compat/compat
120
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
121
+
122
+ act(() => {
123
+ result.current.download(organization.id, agreementId);
124
+ });
125
+
126
+ await waitFor(() => {
127
+ expect(fetchMock.called(DOWNLOAD_URL)).toBe(true);
128
+ expect(mockHandle).toHaveBeenNthCalledWith(1, new Error('Unauthorized'));
129
+ // eslint-disable-next-line compat/compat
130
+ expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
131
+ // eslint-disable-next-line compat/compat
132
+ expect(URL.revokeObjectURL).toHaveBeenCalledTimes(0);
133
+ expect(result.current.loading).toBe(false);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,25 @@
1
+ import { useState } from 'react';
2
+ import { useJoanieApi } from 'contexts/JoanieApiContext';
3
+ import { browserDownloadFromBlob } from 'utils/download';
4
+
5
+ export const useDownloadAgreement = () => {
6
+ const [loading, setLoading] = useState(false);
7
+ const API = useJoanieApi();
8
+
9
+ return {
10
+ download: async (organizationId: string, agreementId: string) => {
11
+ setLoading(true);
12
+ try {
13
+ await browserDownloadFromBlob(() =>
14
+ API.organizations.agreements.download({
15
+ organization_id: organizationId,
16
+ id: agreementId,
17
+ }),
18
+ );
19
+ } finally {
20
+ setLoading(false);
21
+ }
22
+ },
23
+ loading,
24
+ };
25
+ };