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
@@ -1911,6 +1911,14 @@
1911
1911
  "description": "Validate button for the credit card modal",
1912
1912
  "message": "Validate"
1913
1913
  },
1914
+ "components.SaleTunnel.Information.cpf.buttonLabel": {
1915
+ "description": "Label for the button redirecting to Mon Compte Formation",
1916
+ "message": "Go to Mon Compte Formation"
1917
+ },
1918
+ "components.SaleTunnel.Information.cpf.description": {
1919
+ "description": "Explanatory text for the CPF payment option",
1920
+ "message": "Purchase your training course by using your Personal Training Account (CPF) on Mon Compte Formation."
1921
+ },
1914
1922
  "components.SaleTunnel.Information.description": {
1915
1923
  "description": "Description of the information section",
1916
1924
  "message": "This information will be used for billing"
@@ -1951,6 +1959,18 @@
1951
1959
  "description": "Message displayed representing the payment schedule when the order is free.",
1952
1960
  "message": "No payment required. This order is fully covered."
1953
1961
  },
1962
+ "components.SaleTunnel.Information.paymentMode.classic": {
1963
+ "description": "Label for the classic card payment option",
1964
+ "message": "Credit card payment"
1965
+ },
1966
+ "components.SaleTunnel.Information.paymentMode.cpf": {
1967
+ "description": "Label for the CPF (Mon Compte Formation) payment option",
1968
+ "message": "My Training Account (CPF)"
1969
+ },
1970
+ "components.SaleTunnel.Information.paymentMode.title": {
1971
+ "description": "Title for the payment mode selection section",
1972
+ "message": "Payment method"
1973
+ },
1954
1974
  "components.SaleTunnel.Information.paymentSchedule": {
1955
1975
  "description": "Title for the payment schedule section",
1956
1976
  "message": "Payment schedule"
@@ -1911,6 +1911,14 @@
1911
1911
  "description": "Validate button for the credit card modal",
1912
1912
  "message": "Validate"
1913
1913
  },
1914
+ "components.SaleTunnel.Information.cpf.buttonLabel": {
1915
+ "description": "Label for the button redirecting to Mon Compte Formation",
1916
+ "message": "Go to Mon Compte Formation"
1917
+ },
1918
+ "components.SaleTunnel.Information.cpf.description": {
1919
+ "description": "Explanatory text for the CPF payment option",
1920
+ "message": "Purchase your training course by using your Personal Training Account (CPF) on Mon Compte Formation."
1921
+ },
1914
1922
  "components.SaleTunnel.Information.description": {
1915
1923
  "description": "Description of the information section",
1916
1924
  "message": "This information will be used for billing"
@@ -1951,6 +1959,18 @@
1951
1959
  "description": "Message displayed representing the payment schedule when the order is free.",
1952
1960
  "message": "No payment required. This order is fully covered."
1953
1961
  },
1962
+ "components.SaleTunnel.Information.paymentMode.classic": {
1963
+ "description": "Label for the classic card payment option",
1964
+ "message": "Credit card payment"
1965
+ },
1966
+ "components.SaleTunnel.Information.paymentMode.cpf": {
1967
+ "description": "Label for the CPF (Mon Compte Formation) payment option",
1968
+ "message": "My Training Account (CPF)"
1969
+ },
1970
+ "components.SaleTunnel.Information.paymentMode.title": {
1971
+ "description": "Title for the payment mode selection section",
1972
+ "message": "Payment method"
1973
+ },
1954
1974
  "components.SaleTunnel.Information.paymentSchedule": {
1955
1975
  "description": "Title for the payment schedule section",
1956
1976
  "message": "Payment schedule"
package/jest.config.js CHANGED
@@ -17,6 +17,10 @@ module.exports = {
17
17
  transformIgnorePatterns: [
18
18
  'node_modules/(?!(' +
19
19
  'react-intl' +
20
+ '|@formatjs' +
21
+ '|intl-messageformat' +
22
+ '|@tanstack' +
23
+ '|uuid' +
20
24
  '|lodash-es' +
21
25
  '|@hookform/resolvers' +
22
26
  '|query-string' +
@@ -25,6 +29,7 @@ module.exports = {
25
29
  '|filter-obj' +
26
30
  '|@openfun/cunningham-react' +
27
31
  '|keycloak-js' +
32
+ '|@faker-js/faker' +
28
33
  ')/)',
29
34
  ],
30
35
  globals: {
package/js/api/joanie.ts CHANGED
@@ -107,6 +107,10 @@ export const getRoutes = () => {
107
107
  submit_for_payment: {
108
108
  create: `${baseUrl}/batch-orders/:id/submit-for-payment/`,
109
109
  },
110
+ seats: {
111
+ get: `${baseUrl}/batch-orders/:batch_order_id/seats/`,
112
+ },
113
+ seats_export: `${baseUrl}/batch-orders/:id/seats-export/`,
110
114
  },
111
115
  certificates: {
112
116
  download: `${baseUrl}/certificates/:id/download/`,
@@ -161,6 +165,7 @@ export const getRoutes = () => {
161
165
  },
162
166
  agreements: {
163
167
  get: `${baseUrl}/organizations/:organization_id/agreements/:id/`,
168
+ download: `${baseUrl}/organizations/:organization_id/agreements/:id/download/`,
164
169
  },
165
170
  },
166
171
  courses: {
@@ -354,6 +359,17 @@ const API = (): Joanie.API => {
354
359
  ).then(checkStatus);
355
360
  },
356
361
  },
362
+ seats: {
363
+ get: async (filters?: Joanie.BatchOrderSeatsQueryFilters) => {
364
+ return fetchWithJWT(buildApiUrl(ROUTES.user.batchOrders.seats.get, filters)).then(
365
+ checkStatus,
366
+ );
367
+ },
368
+ },
369
+ seats_export: async (id: string): Promise<File> =>
370
+ fetchWithJWT(ROUTES.user.batchOrders.seats_export.replace(':id', id))
371
+ .then(checkStatus)
372
+ .then(getFileFromResponse),
357
373
  },
358
374
  enrollments: {
359
375
  create: async (payload) =>
@@ -545,6 +561,10 @@ const API = (): Joanie.API => {
545
561
  method: 'GET',
546
562
  }).then(checkStatus);
547
563
  },
564
+ download: async (filters: { organization_id: string; id: string }): Promise<File> =>
565
+ fetchWithJWT(buildApiUrl(ROUTES.organizations.agreements.download, filters))
566
+ .then(checkStatus)
567
+ .then(getFileFromResponse),
548
568
  },
549
569
  },
550
570
  courses: {
package/js/api/utils.ts CHANGED
@@ -16,11 +16,12 @@ export async function getFileFromResponse(response: Response): Promise<File> {
16
16
  }
17
17
 
18
18
  export function getResponseBody(response: Response) {
19
- if (response.headers.get('Content-Type') === 'application/json') {
19
+ const contentType = (response.headers.get('Content-Type') || '').split(';')[0].trim();
20
+ if (contentType === 'application/json') {
20
21
  return response.json();
21
22
  }
22
- const fileType = ['application/pdf', 'application/zip'];
23
- if (fileType.includes(response.headers.get('Content-Type') || '')) {
23
+ const fileType = ['application/pdf', 'application/zip', 'text/csv'];
24
+ if (fileType.includes(contentType)) {
24
25
  return new Promise((resolve) => resolve(response));
25
26
  }
26
27
  return response.text();
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { StorybookHelper } from 'utils/StorybookHelper';
3
3
  import { AddressFactory } from 'utils/test/factories/joanie';
4
4
  import AddressForm from '.';
@@ -1,6 +1,6 @@
1
1
  import { yupResolver } from '@hookform/resolvers/yup';
2
2
  import { Fragment, useEffect } from 'react';
3
- import { FormProvider, useForm } from 'react-hook-form';
3
+ import { FormProvider, Resolver, useForm } from 'react-hook-form';
4
4
  import { FormattedMessage, useIntl } from 'react-intl';
5
5
  import { Button, Checkbox } from '@openfun/cunningham-react';
6
6
  import { getLocalizedCunninghamErrorProp } from 'components/Form/utils';
@@ -39,7 +39,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
39
39
  defaultValues: address || defaultValues,
40
40
  mode: 'onBlur',
41
41
  reValidateMode: 'onChange',
42
- resolver: yupResolver(validationSchema),
42
+ resolver: yupResolver(validationSchema) as Resolver<AddressFormValues>,
43
43
  });
44
44
  const { register, handleSubmit, reset, formState } = form;
45
45
 
@@ -140,7 +140,8 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
140
140
  {address ? (
141
141
  <Fragment>
142
142
  <Button
143
- color="tertiary"
143
+ color="brand"
144
+ variant="tertiary"
144
145
  onClick={handleCancel}
145
146
  title={intl.formatMessage(messages.cancelTitleButton)}
146
147
  >
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { StorybookHelper } from 'utils/StorybookHelper';
3
3
  import { RichieContextFactory } from 'utils/test/factories/richie';
4
4
  import AddressesManagement from '.';
@@ -1,4 +1,4 @@
1
- import { Children, useEffect, useState, RefAttributes } from 'react';
1
+ import { Children, useEffect, useState, Ref } from 'react';
2
2
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
3
  import { Button } from '@openfun/cunningham-react';
4
4
  import AddressForm, { type AddressFormValues } from 'components/AddressesManagement/AddressForm';
@@ -105,9 +105,10 @@ export const messages = defineMessages({
105
105
  },
106
106
  });
107
107
 
108
- interface AddressesManagementProps extends RefAttributes<HTMLDivElement> {
108
+ interface AddressesManagementProps {
109
109
  handleClose: () => void;
110
110
  selectAddress: (address: Joanie.Address) => void;
111
+ ref?: Ref<HTMLDivElement>;
111
112
  }
112
113
 
113
114
  const AddressesManagement = ({ handleClose, selectAddress, ref }: AddressesManagementProps) => {
@@ -191,7 +192,8 @@ const AddressesManagement = ({ handleClose, selectAddress, ref }: AddressesManag
191
192
  <div className="AddressesManagement" ref={ref}>
192
193
  <Button
193
194
  className="AddressesManagement__closeButton"
194
- color="tertiary"
195
+ color="brand"
196
+ variant="tertiary"
195
197
  size="small"
196
198
  onClick={handleClose}
197
199
  >
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import Badge from '.';
3
3
 
4
4
  export default {
@@ -1,5 +1,5 @@
1
1
  import classNames from 'classnames';
2
- import { PropsWithChildren } from 'react/ts5.0';
2
+ import { PropsWithChildren } from 'react';
3
3
 
4
4
  type BadgeProps = PropsWithChildren<{
5
5
  color?: 'primary' | 'secondary' | 'tertiary';
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import Banner, { BannerType } 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 { CourseLightFactory, RichieContextFactory } from 'utils/test/factories/richie';
3
3
  import { CourseGlimpse, getCourseGlimpseProps } from 'components/CourseGlimpse';
4
4
  import { CourseCertificateOffer, CourseOffer } from 'types/Course';
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { RichieContextFactory, CourseLightFactory } from 'utils/test/factories/richie';
3
3
  import { CourseGlimpseList, getCourseGlimpseListProps } from '.';
4
4
 
@@ -15,8 +15,8 @@
15
15
  text-align: left;
16
16
  font-size: rem-calc(12px);
17
17
  color: r-theme-val(credit-card-selector, title-color);
18
- font-weight: var(--c--theme--font--weights--bold);
19
- font-family: var(--c--theme--font--families--accent);
18
+ font-weight: var(--c--globals--font--weights--bold);
19
+ font-family: var(--c--globals--font--families--accent);
20
20
  }
21
21
 
22
22
  &__meta {
@@ -111,7 +111,8 @@ export const CreditCardSelector = ({
111
111
  {allowEdit && creditCards?.length > 0 && (
112
112
  <Button
113
113
  icon={<span className="material-icons">edit</span>}
114
- color="tertiary-text"
114
+ color="brand"
115
+ variant="tertiary"
115
116
  size="medium"
116
117
  onClick={modal.open}
117
118
  aria-label={intl.formatMessage(messages.editCreditCardAriaLabel)}
@@ -122,7 +123,8 @@ export const CreditCardSelector = ({
122
123
  <Button
123
124
  onClick={() => setCreditCard(undefined)}
124
125
  size="small"
125
- color="secondary"
126
+ color="brand"
127
+ variant="secondary"
126
128
  className="mt-t"
127
129
  fullWidth={isMobile}
128
130
  >
@@ -217,7 +219,13 @@ const CreditCardSelectorModal = ({
217
219
  size={ModalSize.MEDIUM}
218
220
  title={intl.formatMessage(messages.modalTitle)}
219
221
  actions={
220
- <Button color="primary" size="small" fullWidth={true} onClick={() => onChange(selected)}>
222
+ <Button
223
+ color="brand"
224
+ variant="primary"
225
+ size="small"
226
+ fullWidth={true}
227
+ onClick={() => onChange(selected)}
228
+ >
221
229
  <FormattedMessage {...messages.modalTitle} />
222
230
  </Button>
223
231
  }
@@ -0,0 +1,51 @@
1
+ import { useId } from 'react';
2
+ import { Button } from '@openfun/cunningham-react';
3
+ import { FormattedMessage, defineMessages } from 'react-intl';
4
+ import { Spinner } from 'components/Spinner';
5
+ import { useDownloadAgreement } from 'hooks/useDownloadAgreement';
6
+
7
+ const messages = defineMessages({
8
+ download: {
9
+ defaultMessage: 'Download agreement',
10
+ description: 'Label for the button to download a signed agreement PDF',
11
+ id: 'components.DownloadAgreementButton.download',
12
+ },
13
+ generating: {
14
+ defaultMessage: 'Downloading...',
15
+ description: 'Accessible label displayed while agreement PDF is being downloaded.',
16
+ id: 'components.DownloadAgreementButton.generating',
17
+ },
18
+ });
19
+
20
+ interface DownloadAgreementButtonProps {
21
+ organizationId: string;
22
+ agreementId: string;
23
+ }
24
+
25
+ const DownloadAgreementButton = ({ organizationId, agreementId }: DownloadAgreementButtonProps) => {
26
+ const { download, loading } = useDownloadAgreement();
27
+ const labelId = useId();
28
+
29
+ return (
30
+ <Button
31
+ size="small"
32
+ color="brand"
33
+ variant="primary"
34
+ className="dashboard-item__action-button"
35
+ disabled={loading}
36
+ onClick={() => download(organizationId, agreementId)}
37
+ >
38
+ {loading ? (
39
+ <Spinner theme="primary" aria-labelledby={labelId}>
40
+ <span id={labelId}>
41
+ <FormattedMessage {...messages.generating} />
42
+ </span>
43
+ </Spinner>
44
+ ) : (
45
+ <FormattedMessage {...messages.download} />
46
+ )}
47
+ </Button>
48
+ );
49
+ };
50
+
51
+ export default DownloadAgreementButton;
@@ -0,0 +1,46 @@
1
+ import { buildFilename, sanitizeForFilename } from '.';
2
+
3
+ describe('sanitizeForFilename', () => {
4
+ it('replaces spaces with underscores', () => {
5
+ expect(sanitizeForFilename('Formation React')).toBe('Formation_React');
6
+ });
7
+
8
+ it('removes diacritics', () => {
9
+ expect(sanitizeForFilename('Développement web')).toBe('Developpement_web');
10
+ });
11
+
12
+ it('removes special characters', () => {
13
+ expect(sanitizeForFilename('C++ / Python')).toBe('C_Python');
14
+ });
15
+
16
+ it('preserves hyphens', () => {
17
+ expect(sanitizeForFilename('Formation React - Advanced')).toBe('Formation_React_-_Advanced');
18
+ });
19
+
20
+ it('trims leading and trailing spaces', () => {
21
+ expect(sanitizeForFilename(' Formation ')).toBe('Formation');
22
+ });
23
+ });
24
+
25
+ describe('buildFilename', () => {
26
+ beforeEach(() => {
27
+ jest.useFakeTimers();
28
+ jest.setSystemTime(new Date('2026-04-15T09:30:00Z'));
29
+ });
30
+
31
+ afterEach(() => {
32
+ jest.useRealTimers();
33
+ });
34
+
35
+ it('builds the expected filename', () => {
36
+ expect(buildFilename('seats', 'Formation React')).toBe(
37
+ 'seats_Formation_React_2026-04-15_09-30.csv',
38
+ );
39
+ });
40
+
41
+ it('sanitizes the product title in the filename', () => {
42
+ expect(buildFilename('seats', 'Développement web avancé')).toBe(
43
+ 'seats_Developpement_web_avance_2026-04-15_09-30.csv',
44
+ );
45
+ });
46
+ });
@@ -0,0 +1,80 @@
1
+ import { useId } from 'react';
2
+ import { Button } from '@openfun/cunningham-react';
3
+ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
4
+ import { Spinner } from 'components/Spinner';
5
+ import { useDownloadBatchOrderSeats } from 'hooks/useDownloadBatchOrderSeats';
6
+
7
+ const messages = defineMessages({
8
+ download: {
9
+ defaultMessage: 'Export CSV',
10
+ description: 'Label for the button to export batch order seats as CSV',
11
+ id: 'components.DownloadBatchOrderSeatsButton.download',
12
+ },
13
+ generating: {
14
+ defaultMessage: 'Generating export...',
15
+ description: 'Accessible label displayed while CSV export is being generated.',
16
+ id: 'components.DownloadBatchOrderSeatsButton.generating',
17
+ },
18
+ seats: {
19
+ defaultMessage: 'Seats',
20
+ description: 'Text displayed for seats value in batch order',
21
+ id: 'batchOrder.seats',
22
+ },
23
+ });
24
+
25
+ export const sanitizeForFilename = (str: string) =>
26
+ str
27
+ .normalize('NFD')
28
+ .replace(/[\u0300-\u036f]/g, '')
29
+ .replace(/[^a-zA-Z0-9\s-]/g, '')
30
+ .trim()
31
+ .replace(/\s+/g, '_');
32
+
33
+ export const buildFilename = (prefix: string, productTitle: string) => {
34
+ const now = new Date();
35
+ const date = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`;
36
+ const time = `${String(now.getUTCHours()).padStart(2, '0')}-${String(now.getUTCMinutes()).padStart(2, '0')}`;
37
+ return `${prefix}_${sanitizeForFilename(productTitle)}_${date}_${time}.csv`;
38
+ };
39
+
40
+ interface DownloadBatchOrderSeatsButtonProps {
41
+ batchOrderId: string;
42
+ productTitle: string;
43
+ }
44
+
45
+ const DownloadBatchOrderSeatsButton = ({
46
+ batchOrderId,
47
+ productTitle,
48
+ }: DownloadBatchOrderSeatsButtonProps) => {
49
+ const { download, loading } = useDownloadBatchOrderSeats();
50
+ const labelId = useId();
51
+ const intl = useIntl();
52
+
53
+ const handleClick = () => {
54
+ const prefix = intl.formatMessage(messages.seats);
55
+ download(batchOrderId, buildFilename(prefix, productTitle));
56
+ };
57
+
58
+ return (
59
+ <Button
60
+ size="small"
61
+ color="brand"
62
+ variant="primary"
63
+ className="dashboard-item__action-button"
64
+ disabled={loading}
65
+ onClick={handleClick}
66
+ >
67
+ {loading ? (
68
+ <Spinner theme="primary" aria-labelledby={labelId}>
69
+ <span id={labelId}>
70
+ <FormattedMessage {...messages.generating} />
71
+ </span>
72
+ </Spinner>
73
+ ) : (
74
+ <FormattedMessage {...messages.download} />
75
+ )}
76
+ </Button>
77
+ );
78
+ };
79
+
80
+ export default DownloadBatchOrderSeatsButton;
@@ -38,7 +38,8 @@ const DownloadCertificateButton = ({
38
38
  <Button
39
39
  className={className}
40
40
  size="small"
41
- color="secondary"
41
+ color="brand"
42
+ variant="secondary"
42
43
  disabled={loading}
43
44
  onClick={onDownloadClick}
44
45
  >
@@ -39,7 +39,13 @@ const DownloadContractButton = ({ contract, className }: DownloadContractButtonP
39
39
  };
40
40
 
41
41
  return (
42
- <Button size="small" className={className} color="secondary" onClick={downloadContract}>
42
+ <Button
43
+ size="small"
44
+ className={className}
45
+ color="brand"
46
+ variant="secondary"
47
+ onClick={downloadContract}
48
+ >
43
49
  <FormattedMessage {...messages.contractDownloadActionLabel} />
44
50
  </Button>
45
51
  );
@@ -1,8 +1,10 @@
1
1
  import c from 'classnames';
2
2
  import { PropsWithChildren } from 'react';
3
3
 
4
- interface FormProps
5
- extends React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> {}
4
+ interface FormProps extends React.DetailedHTMLProps<
5
+ React.FormHTMLAttributes<HTMLFormElement>,
6
+ HTMLFormElement
7
+ > {}
6
8
 
7
9
  const Form = ({ children, onSubmit, className, name, noValidate = true }: FormProps) => {
8
10
  return (
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { PropsWithChildren, useState, useRef, CSSProperties } from 'react';
3
3
  import { HttpStatusCode } from 'utils/errors/HttpError';
4
4
  import { Icon, IconTypeEnum } from './index';
@@ -69,6 +69,7 @@ const IconContainer = ({ name, enumKey }: IconContainerProps) => {
69
69
  };
70
70
 
71
71
  const clipboardCopy = () => {
72
+ // eslint-disable-next-line compat/compat
72
73
  navigator.clipboard.writeText(`${ENUM_NAME}.${enumKey}`);
73
74
  setShowTooltip(true);
74
75
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
@@ -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 { Modal } from './index';
4
4
 
@@ -78,7 +78,8 @@ export const Modal = ({
78
78
  className="modal__closeButton"
79
79
  onClick={(e) => props.onRequestClose?.(e)}
80
80
  title={intl.formatMessage(messages.closeDialog)}
81
- color="tertiary"
81
+ color="brand"
82
+ variant="tertiary"
82
83
  size="small"
83
84
  >
84
85
  <Icon name={IconTypeEnum.ROUND_CLOSE} />
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
3
3
  import OpenEdxFullNameForm from '.';
4
4
 
@@ -1,6 +1,6 @@
1
1
  import { ButtonElement, Input, Alert, VariantType } from '@openfun/cunningham-react';
2
2
  import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
3
- import { FormProvider, useForm } from 'react-hook-form';
3
+ import { FormProvider, Resolver, useForm } from 'react-hook-form';
4
4
  import * as Yup from 'yup';
5
5
  import { yupResolver } from '@hookform/resolvers/yup';
6
6
  import { useEffect, useMemo, useRef } from 'react';
@@ -78,7 +78,7 @@ const OpenEdxFullNameForm = () => {
78
78
  defaultValues,
79
79
  mode: 'onBlur',
80
80
  reValidateMode: 'onChange',
81
- resolver: yupResolver(validationSchema),
81
+ resolver: yupResolver(validationSchema) as Resolver<OpenEdxFullNameFormValues>,
82
82
  });
83
83
 
84
84
  const { getValues, register, handleSubmit, reset, formState } = form;
@@ -18,8 +18,8 @@
18
18
  height: 24px;
19
19
  padding: 0 12px;
20
20
  border-radius: 24px;
21
- font-family: var(--c--theme--font--families--accent);
22
- font-weight: var(--c--theme--font--weight--semibold);
21
+ font-family: var(--c--globals--font--families--accent);
22
+ font-weight: var(--c--globals--font--weights--medium);
23
23
  font-size: rem-calc(12px);
24
24
 
25
25
  &--canceled,
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { ProductFactory } from 'utils/test/factories/joanie';
3
3
  import PurchaseButton from '.';
4
4
 
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { StorybookHelper } from 'utils/StorybookHelper';
3
3
  import { AddressFactory } from 'utils/test/factories/joanie';
4
4
  import RegisteredAddress from '.';
@@ -91,7 +91,8 @@ const RegisteredAddress = ({ promote, select, edit, remove, address }: Props) =>
91
91
  </Button>
92
92
  <Button
93
93
  aria-label={intl.formatMessage(messages.editButtonLabel, { title: address.title })}
94
- color="secondary"
94
+ color="brand"
95
+ variant="secondary"
95
96
  size="small"
96
97
  onClick={() => edit(address)}
97
98
  >
@@ -101,7 +102,8 @@ const RegisteredAddress = ({ promote, select, edit, remove, address }: Props) =>
101
102
  aria-label={intl.formatMessage(messages.deleteButtonLabel, {
102
103
  title: address.title,
103
104
  })}
104
- color="secondary"
105
+ color="brand"
106
+ variant="secondary"
105
107
  size="small"
106
108
  disabled={address.is_main}
107
109
  onClick={() => remove(address)}
@@ -52,7 +52,7 @@ export const CreateAddressFormModal = (props: AddressFormModalProps) => {
52
52
  size={ModalSize.MEDIUM}
53
53
  title={intl.formatMessage(messages.title)}
54
54
  actions={
55
- <Button color="primary" size="small" onClick={handleSubmit(onSubmit)}>
55
+ <Button color="brand" variant="primary" size="small" onClick={handleSubmit(onSubmit)}>
56
56
  <FormattedMessage {...messages.submit} />
57
57
  </Button>
58
58
  }
@@ -49,7 +49,7 @@ export const EditAddressFormModal = ({
49
49
  size={ModalSize.MEDIUM}
50
50
  title={intl.formatMessage(messages.title)}
51
51
  actions={
52
- <Button color="primary" size="small" onClick={handleSubmit(onSubmit)}>
52
+ <Button color="brand" variant="primary" size="small" onClick={handleSubmit(onSubmit)}>
53
53
  <FormattedMessage {...messages.save} />
54
54
  </Button>
55
55
  }