richie-education 2.33.1-dev9 → 2.34.1-dev2

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 (60) hide show
  1. package/i18n/locales/ar-SA.json +48 -8
  2. package/i18n/locales/es-ES.json +48 -8
  3. package/i18n/locales/fa-IR.json +48 -8
  4. package/i18n/locales/fr-CA.json +48 -8
  5. package/i18n/locales/fr-FR.json +48 -8
  6. package/i18n/locales/ko-KR.json +48 -8
  7. package/i18n/locales/pt-PT.json +55 -15
  8. package/i18n/locales/ru-RU.json +48 -8
  9. package/i18n/locales/vi-VN.json +48 -8
  10. package/js/api/joanie.ts +5 -0
  11. package/js/api/lms/dummy.ts +12 -10
  12. package/js/components/AddressesManagement/index.spec.tsx +1 -1
  13. package/js/components/AddressesManagement/index.tsx +123 -129
  14. package/js/components/ContractFrame/AbstractContractFrame.tsx +1 -1
  15. package/js/components/Icon/index.stories.tsx +1 -1
  16. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  17. package/js/components/PaymentScheduleGrid/_styles.scss +6 -5
  18. package/js/components/PaymentScheduleGrid/index.tsx +16 -0
  19. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +2 -2
  20. package/js/components/SearchInput/index.spec.tsx +6 -5
  21. package/js/components/SearchInput/index.tsx +9 -1
  22. package/js/hooks/useCreditCards/index.spec.tsx +83 -0
  23. package/js/hooks/useCreditCards/index.ts +53 -1
  24. package/js/hooks/useCreditCardsManagement.tsx +1 -10
  25. package/js/hooks/useLearnerCoursesSearch/index.tsx +2 -2
  26. package/js/pages/DashboardCourses/index.spec.tsx +51 -7
  27. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -5
  28. package/js/pages/DashboardCreditCardsManagement/_styles.scss +11 -3
  29. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +46 -25
  30. package/js/pages/DashboardCreditCardsManagement/index.tsx +21 -37
  31. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -2
  32. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +1 -1
  33. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +8 -5
  34. package/js/translations/ar-SA.json +1 -1
  35. package/js/translations/es-ES.json +1 -1
  36. package/js/translations/fa-IR.json +1 -1
  37. package/js/translations/fr-CA.json +1 -1
  38. package/js/translations/fr-FR.json +1 -1
  39. package/js/translations/ko-KR.json +1 -1
  40. package/js/translations/pt-PT.json +1 -1
  41. package/js/translations/ru-RU.json +1 -1
  42. package/js/translations/vi-VN.json +1 -1
  43. package/js/types/Joanie.ts +13 -1
  44. package/js/utils/OrderHelper/index.ts +9 -0
  45. package/js/utils/errors/HttpError.ts +1 -0
  46. package/js/utils/test/wrappers/types.ts +2 -2
  47. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +7 -1
  48. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +14 -2
  49. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +3 -2
  50. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +12 -5
  51. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +1 -1
  52. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +5 -0
  53. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +10 -8
  54. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +5 -0
  55. package/js/widgets/Dashboard/components/SearchBar/index.spec.tsx +1 -1
  56. package/js/widgets/Dashboard/components/SearchResultsCount/index.spec.tsx +1 -1
  57. package/js/widgets/Dashboard/index.spec.tsx +7 -1
  58. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +1 -5
  59. package/package.json +41 -39
  60. package/scss/vendors/css/cunningham-tokens.css +1 -0
@@ -507,10 +507,6 @@
507
507
  "description": "Empty placeholder of the dashboard credit cards management block",
508
508
  "message": "You haven't created any credit cards yet."
509
509
  },
510
- "components.DashboardCreditCardsManagement.errorCannotPromoteMain": {
511
- "description": "Error shown if a user tries to promote a main credit card",
512
- "message": "Cannot promote main credit card."
513
- },
514
510
  "components.DashboardCreditCardsManagement.header": {
515
511
  "description": "Title of the dashboard credit cards management block",
516
512
  "message": "Credit cards"
@@ -627,6 +623,10 @@
627
623
  "description": "Status shown on the dashboard order item when order is pending for payment",
628
624
  "message": "On going"
629
625
  },
626
+ "components.DashboardItem.Order.OrderStateLearnerMessage.statusRefunded": {
627
+ "description": "Status shown on the dashboard order item when order is refunded",
628
+ "message": "Refunded"
629
+ },
630
630
  "components.DashboardItem.Order.OrderStateLearnerMessage.statusWaitingCounterSignature": {
631
631
  "description": "Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
632
632
  "message": "On going"
@@ -675,6 +675,10 @@
675
675
  "description": "Status shown on the dashboard order item when order is validated with no certificate",
676
676
  "message": "On going"
677
677
  },
678
+ "components.DashboardItem.Order.OrderStateTeacherMessage.statusRefunded": {
679
+ "description": "Status shown on the dashboard order item when order is refunded",
680
+ "message": "Refunded"
681
+ },
678
682
  "components.DashboardItem.Order.OrderStateTeacherMessage.statusWaitingCounterSignature": {
679
683
  "description": "Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
680
684
  "message": "To be signed"
@@ -1187,6 +1191,14 @@
1187
1191
  "description": "Label for screen reader when a credit card is being tokenized.",
1188
1192
  "message": "Payment method definition in progress."
1189
1193
  },
1194
+ "components.PaymentScheduleGrid.state.canceled": {
1195
+ "description": "Label displayed for canceled payment state",
1196
+ "message": "Canceled"
1197
+ },
1198
+ "components.PaymentScheduleGrid.state.error": {
1199
+ "description": "Label displayed for error payment state. For learner we assume to display `pending`.",
1200
+ "message": "Pending"
1201
+ },
1190
1202
  "components.PaymentScheduleGrid.state.paid": {
1191
1203
  "description": "Label displayed for paid payment state",
1192
1204
  "message": "Paid"
@@ -1195,6 +1207,10 @@
1195
1207
  "description": "Label displayed for pending payment state",
1196
1208
  "message": "Pending"
1197
1209
  },
1210
+ "components.PaymentScheduleGrid.state.refunded": {
1211
+ "description": "Label displayed for refunded payment state",
1212
+ "message": "Refunded"
1213
+ },
1198
1214
  "components.PaymentScheduleGrid.state.refused": {
1199
1215
  "description": "Label displayed for refused payment state",
1200
1216
  "message": "Refused"
@@ -1527,6 +1543,10 @@
1527
1543
  "description": "Accessibility text for the search button inside the Search input.",
1528
1544
  "message": "Search"
1529
1545
  },
1546
+ "components.SearchInput.label": {
1547
+ "description": "Accessibility text for the search input label.",
1548
+ "message": "Search"
1549
+ },
1530
1550
  "components.SearchSuggestField.searchFieldPlaceholder": {
1531
1551
  "description": "Placeholder text displayed in the search field when it is empty.",
1532
1552
  "message": "Search for courses, organizations, categories"
@@ -1611,6 +1631,10 @@
1611
1631
  "description": "Course date of an opened course run block",
1612
1632
  "message": "From {startDate} {endDate, select, undefined {} other {to {endDate}}}"
1613
1633
  },
1634
+ "components.SyllabusCourseRun.enrollNow": {
1635
+ "description": "CTA for users to enroll on ongoing of future open course.",
1636
+ "message": "Enroll now"
1637
+ },
1614
1638
  "components.SyllabusCourseRun.enrollment": {
1615
1639
  "description": "Title of the enrollment dates section of an opened course run block",
1616
1640
  "message": "Enrollment"
@@ -1623,10 +1647,18 @@
1623
1647
  "description": "Title of the languages section of an opened course run block",
1624
1648
  "message": "Languages"
1625
1649
  },
1650
+ "components.SyllabusCourseRun.studyNow": {
1651
+ "description": "CTA for users to enroll on archived course.",
1652
+ "message": "Study now"
1653
+ },
1626
1654
  "components.SyllabusCourseRunCompacted.course": {
1627
1655
  "description": "Title of the course dates section of an opened course run block",
1628
1656
  "message": "Course"
1629
1657
  },
1658
+ "components.SyllabusCourseRunCompacted.enrollNow": {
1659
+ "description": "CTA for users to enroll on ongoing of future open course.",
1660
+ "message": "Enroll now"
1661
+ },
1630
1662
  "components.SyllabusCourseRunCompacted.languages": {
1631
1663
  "description": "Title of the languages section of an opened course run block",
1632
1664
  "message": "Languages"
@@ -1639,6 +1671,10 @@
1639
1671
  "description": "Self paced course run block with no end date",
1640
1672
  "message": "Available"
1641
1673
  },
1674
+ "components.SyllabusCourseRunCompacted.studyNow": {
1675
+ "description": "CTA for users to enroll on archived course.",
1676
+ "message": "Study now"
1677
+ },
1642
1678
  "components.SyllabusCourseRunsList.multipleOpenedCourseRuns": {
1643
1679
  "description": "Message displayed when there are multiple opened course runs on a syllabus",
1644
1680
  "message": "{count} course runs are currently open for this course"
@@ -1955,6 +1991,10 @@
1955
1991
  "description": "Error message shown to the user when course fetch request fails.",
1956
1992
  "message": "An error occurred while fetching course. Please retry later."
1957
1993
  },
1994
+ "hooks.useCreditCards.errorCannotDelete": {
1995
+ "description": "Error message shown to the user when trying to delete a credit card that is used to pay at least order.",
1996
+ "message": "Cannot delete the credit card •••• •••• •••• {last_numbers} because it is used to pay at least one of your order."
1997
+ },
1958
1998
  "hooks.useCreditCards.errorDelete": {
1959
1999
  "description": "Error message shown to the user when credit card deletion request fails.",
1960
2000
  "message": "An error occurred while deleting the credit card. Please retry later."
@@ -1963,6 +2003,10 @@
1963
2003
  "description": "Error message shown to the user when no credit cards matches.",
1964
2004
  "message": "Cannot find the credit card"
1965
2005
  },
2006
+ "hooks.useCreditCards.errorPromote": {
2007
+ "description": "Error message shown to the user when promoting a credit card fails.",
2008
+ "message": "Cannot set the credit card as default"
2009
+ },
1966
2010
  "hooks.useCreditCards.errorSelect": {
1967
2011
  "description": "Error message shown to the user when credit cards fetch request fails.",
1968
2012
  "message": "An error occurred while fetching credit cards. Please retry later."
@@ -1979,10 +2023,6 @@
1979
2023
  "description": "Confirmation message shown to the user when he wants to delete a credit card",
1980
2024
  "message": "Are you sure you want to delete the credit card? ⚠️ You cannot undo this change after."
1981
2025
  },
1982
- "hooks.useCreditCardsManagement.errorCannotRemoveMain": {
1983
- "description": "Error shown if a user tries to delete a main credit card",
1984
- "message": "Cannot remove main credit card."
1985
- },
1986
2026
  "hooks.useDashboardAddressForm.isMainInputLabel": {
1987
2027
  "description": "Label of the \"is_main\" input",
1988
2028
  "message": "Use this address as default"
package/js/api/joanie.ts CHANGED
@@ -83,6 +83,7 @@ export const getRoutes = () => {
83
83
  update: `${baseUrl}/credit-cards/:id/`,
84
84
  delete: `${baseUrl}/credit-cards/:id/`,
85
85
  tokenize: `${baseUrl}/credit-cards/tokenize-card/`,
86
+ promote: `${baseUrl}/credit-cards/:id/promote/`,
86
87
  },
87
88
  addresses: {
88
89
  get: `${baseUrl}/addresses/:id/`,
@@ -242,6 +243,10 @@ const API = (): Joanie.API => {
242
243
  }).then(checkStatus),
243
244
  tokenize: async () =>
244
245
  fetchWithJWT(ROUTES.user.creditCards.tokenize, { method: 'POST' }).then(checkStatus),
246
+ promote: async (id) =>
247
+ fetchWithJWT(ROUTES.user.creditCards.promote.replace(':id', id), {
248
+ method: 'PATCH',
249
+ }).then(checkStatus),
245
250
  },
246
251
  addresses: {
247
252
  get: (id?: string) => {
@@ -25,24 +25,26 @@ type JWTPayload = {
25
25
  username: string;
26
26
  };
27
27
 
28
- /* All JWT tokens will expire the 12 Dec 2024 ! */
28
+ /* All JWT tokens will expire the 02 Feb 2026 ! */
29
29
  const JOANIE_DEV_DEMO_USER_JWT_TOKENS = {
30
- admin:
31
- 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzNjU1MjMsImp0aSI6IjRhMzQxZWVmMmVhOTRkNGFiMzQ5OThkOWE4ZDM5MTI0IiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6ImFkbWluIiwiZnVsbF9uYW1lIjoiIn0.rT8nymp8f4T7tIIXO-M5-ahXBwxoDNVqtaZIrb_GHuk',
32
30
  user0:
33
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6Ijc2ZDNlNmU2NGYwMzQ4NTg5NTNiOWIxNWIwNDhhNjI0IiwiZW1haWwiOiJjc3RlcGhlbnNvbkBleGFtcGxlLm9yZyIsImxhbmd1YWdlIjoiZnItZnIiLCJ1c2VybmFtZSI6InVzZXIwIiwiZnVsbF9uYW1lIjoiT3RoZXIgT3duZXIifQ.JQRHKdr3Utl9rw-BQyvM8zXsY16CSgscEh9NAaP2INc',
31
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImQwZmU1Zjg5ZjFhYTQ4YmM5NDhmNWU4ODFkNTNhNTU2IiwiZW1haWwiOiJwc21pdGhAZXhhbXBsZS5vcmciLCJsYW5ndWFnZSI6ImVuLXVzIiwidXNlcm5hbWUiOiJ1c2VyMCIsImZ1bGxfbmFtZSI6Ik90aGVyIE93bmVyIn0.eCawfaCzpO7U7iUPC1TE_XYDiRjq_crI93GqE8Fj8zc',
34
32
  user1:
35
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6IjY3NDBmYzFlOTlhNDQwNzBhN2I1NWNkMTE0M2UzNThhIiwiZW1haWwiOiJuYW5jeTgzQGV4YW1wbGUuY29tIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjEiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.mWtt4zKA-SiSQG2Pfrauq9ZYzSCq53qPa0jjdGaqFWQ',
33
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImIwYjk3YjZkZjFlMzRkMTg4NjFiMGFhMjcxYWI0YWU1IiwiZW1haWwiOiJzYW1wc29uYW5uYUBleGFtcGxlLm9yZyIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6InVzZXIxIiwiZnVsbF9uYW1lIjoiT3RoZXIgT3duZXIifQ.yd46_63iuw19zmzH8aVNRAVAvAE4VGH8W8BjmFs6PPU',
36
34
  user2:
37
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6IjBhNjkwY2QyZjA5ZTRjMWU4MTE4MzhmNGI0YjEzNGMyIiwiZW1haWwiOiJwYXVsYnJhbmRvbkBleGFtcGxlLm9yZyIsImxhbmd1YWdlIjoiZnItZnIiLCJ1c2VybmFtZSI6InVzZXIyIiwiZnVsbF9uYW1lIjoiT3RoZXIgT3duZXIifQ.EXUlkR-0IIj8xRRnlf9TWyVa8Gh_jPE7aNIu18BI83Q',
35
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImNmYzY2OTNmY2Q5ZTRlZGViM2Y2NzU1MTZhNDIzMTdiIiwiZW1haWwiOiJsb3BlemFtYmVyQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjIiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.TlFILOXY-wK29M_BUgDKgjdOovSfEIlw5cNXed6ZV3w',
38
36
  user3:
39
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6IjQ5N2IyMTEzYjQ4MjQ2YWNhOWUyMWIyMmZmZGJkYWU5IiwiZW1haWwiOiJqZXNzaWNhNDFAZXhhbXBsZS5jb20iLCJsYW5ndWFnZSI6ImVuLXVzIiwidXNlcm5hbWUiOiJ1c2VyMyIsImZ1bGxfbmFtZSI6Ik90aGVyIE93bmVyIn0.MXEFrvlkxPxlCpS172ls4eNvbV6vEDBJK1T3PHS9CoY',
37
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6IjA4ZTcxZGJjYWIyMDRjMmZhZjgyMDVjZTRiNTliMjZiIiwiZW1haWwiOiJsb25nZWxpemFiZXRoQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjMiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.8NxYyjc567lO2Yc7me-TQr8PNvKqB5VLRzHd1Z4vA4U',
40
38
  user4:
41
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6ImMyYTBmMGU0NTI5MTRmNWFhZDdiYTE1YWQ3ZTQ5MjgwIiwiZW1haWwiOiJkb25uYTE5QGV4YW1wbGUubmV0IiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjQiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.sVmYGo2LO8I1Sz2hP8wRk8yd0n1bOcWsyFG0ZXi5SbE',
39
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImVmZGRkM2Q0YTdmZDQ4ZmFhYmZkM2Q2OTI4YzMwM2U4IiwiZW1haWwiOiJqb25lc2plbm5pZmVyQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjQiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.Wn5CKuNPn0s4B_76Mxd3zTKqdUMaZGV456bhZ-fDe-o',
42
40
  organization_owner:
43
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6IjYxNTZiYmNmMjZmMzQzYTBiYTgzM2ZmYzU1M2U1NjBlIiwiZW1haWwiOiJqZWFuLWJhcHRpc3RlLnBlbnJhdGgrb3JnYW5pemF0aW9uX293bmVyQGZ1bi1tb29jLmZyIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoib3JnYW5pemF0aW9uX293bmVyIiwiZnVsbF9uYW1lIjoiT3JnYSBPd25lciBPd25lciJ9.wkdYFBasBIk4U-Vr7SdnkfgoqAlhn7rmr0Bqcs777_w',
41
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImRiYmU2ZGExZjhmNDQzNDA4N2U2NzQ0YTIzM2JmNjFiIiwiZW1haWwiOiJkZXZlbG9wZXIrb3JnYW5pemF0aW9uX293bmVyQGV4YW1wbGUuY29tIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoib3JnYW5pemF0aW9uX293bmVyIiwiZnVsbF9uYW1lIjoiT3JnYSBPd25lciJ9.a6QjOAOxCw7ZFKvg8OCcUaW8Xhbmfuqy3cwIqUCPfzE',
44
42
  student_user:
45
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzOTIyMTQsImp0aSI6ImNkZjAyMGM4ODdjOTQxYzU5ZmExN2FkZGExNjNjMDIzIiwiZW1haWwiOiJqZWFuLWJhcHRpc3RlLnBlbnJhdGgrc3R1ZGVudF91c2VyQGZ1bi1tb29jLmZyIiwibGFuZ3VhZ2UiOiJmci1mciIsInVzZXJuYW1lIjoic3R1ZGVudF91c2VyIiwiZnVsbF9uYW1lIjoiXHUwMGM5dHVkaWFudCJ9.JMdnC2VXwq2VbNPrIYxj8PEq0oJJ4LZZT_ywWyE1lBM',
43
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6IjNhMGExYjM0OWEwNDQxNTg5ODU4NGUwZjMwNTc5M2EwIiwiZW1haWwiOiJkZXZlbG9wZXIrc3R1ZGVudF91c2VyQGV4YW1wbGUuY29tIiwibGFuZ3VhZ2UiOiJmci1mciIsInVzZXJuYW1lIjoic3R1ZGVudF91c2VyIiwiZnVsbF9uYW1lIjoiXHUwMGM5dHVkaWFudCJ9.3VvjPXwtuNA684hSIem3X2uFD-4WH8fipVDXMsi1cAc',
44
+ second_student_user:
45
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6Ijg5ZDIyNDJjODRkODRiNThiZWVkYjg1NmU2MGNiM2FiIiwiZW1haWwiOiJkZXZlbG9wZXIrc2Vjb25kX3N0dWRlbnRfdXNlckBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZnItZnIiLCJ1c2VybmFtZSI6InNlY29uZF9zdHVkZW50X3VzZXIiLCJmdWxsX25hbWUiOiJcdTAwYzl0dWRpYW50IDAwMiJ9.p5p4Ku0w8mHortWW9TYHTJgORF9wnfCpq-6pvBRjU0Y',
46
+ admin:
47
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6Ijk4M2UzNmI5MTUzODQ2Mjg4ZGMxNWNjOTAwNDgwMDA4IiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6ImFkbWluIiwiZnVsbF9uYW1lIjoiIn0.VuSqfh4l0vtIDSdkEgCNyciiOhlFlMAsf5u5snm2Avw',
46
48
  };
47
49
 
48
50
  export type DevDemoUser = keyof typeof JOANIE_DEV_DEMO_USER_JWT_TOKENS;
@@ -6,7 +6,7 @@ import fetchMock from 'fetch-mock';
6
6
  import { IntlProvider } from 'react-intl';
7
7
  import countries from 'i18n-iso-countries';
8
8
  import { QueryClientProvider } from '@tanstack/react-query';
9
- import { PropsWithChildren } from 'react';
9
+ import React, { PropsWithChildren } from 'react';
10
10
  import { CunninghamProvider } from '@openfun/cunningham-react';
11
11
  import userEvent, { UserEvent } from '@testing-library/user-event';
12
12
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
@@ -1,4 +1,4 @@
1
- import { Children, forwardRef, useEffect, useState } from 'react';
1
+ import { Children, useEffect, useState, RefAttributes } 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';
@@ -7,7 +7,6 @@ import { Icon, IconTypeEnum } from 'components/Icon';
7
7
  import RegisteredAddress from 'components/RegisteredAddress';
8
8
  import { useAddressesManagement } from 'hooks/useAddressesManagement';
9
9
  import type * as Joanie from 'types/Joanie';
10
- import { Address } from 'types/Joanie';
11
10
  import { Maybe } from 'types/utils';
12
11
 
13
12
  // constant used as `address.id` for local address
@@ -106,147 +105,142 @@ export const messages = defineMessages({
106
105
  },
107
106
  });
108
107
 
109
- interface AddressesManagementProps {
108
+ interface AddressesManagementProps extends RefAttributes<HTMLDivElement> {
110
109
  handleClose: () => void;
111
110
  selectAddress: (address: Joanie.Address) => void;
112
111
  }
113
112
 
114
- const AddressesManagement = forwardRef<HTMLDivElement, AddressesManagementProps>(
115
- ({ handleClose, selectAddress }, ref) => {
116
- const intl = useIntl();
117
- const [editedAddress, setEditedAddress] = useState<Maybe<Joanie.Address>>();
118
- const {
119
- methods: { setError, create, update, remove, promote },
120
- states: { error },
121
- ...addresses
122
- } = useAddressesManagement();
113
+ const AddressesManagement = ({ handleClose, selectAddress, ref }: AddressesManagementProps) => {
114
+ const intl = useIntl();
115
+ const [editedAddress, setEditedAddress] = useState<Maybe<Joanie.Address>>();
116
+ const {
117
+ methods: { setError, create, update, remove, promote },
118
+ states: { error },
119
+ ...addresses
120
+ } = useAddressesManagement();
123
121
 
124
- /**
125
- * Sort addresses ascending by title according to the locale
126
- *
127
- * @param {Joanie.Address} a
128
- * @param {Joanie.Address} b
129
- * @returns {Joanie.Address[]} Sorted addresses ascending by title
130
- */
131
- const sortAddressByTitleAsc = (a: Joanie.Address, b: Joanie.Address) => {
132
- return a.title.localeCompare(b.title, [intl.locale, intl.defaultLocale]);
133
- };
122
+ /**
123
+ * Sort addresses ascending by title according to the locale
124
+ *
125
+ * @param {Joanie.Address} a
126
+ * @param {Joanie.Address} b
127
+ * @returns {Joanie.Address[]} Sorted addresses ascending by title
128
+ */
129
+ const sortAddressByTitleAsc = (a: Joanie.Address, b: Joanie.Address) => {
130
+ return a.title.localeCompare(b.title, [intl.locale, intl.defaultLocale]);
131
+ };
134
132
 
135
- /**
136
- * update `selectedAddress` state with the address provided
137
- * then close the address management form
138
- *
139
- * @param {Joanie.Address} address
140
- */
141
- const handleSelect = (address: Joanie.Address) => {
142
- setError(undefined);
143
- selectAddress(address);
144
- handleClose();
145
- };
133
+ /**
134
+ * update `selectedAddress` state with the address provided
135
+ * then close the address management form
136
+ *
137
+ * @param {Joanie.Address} address
138
+ */
139
+ const handleSelect = (address: Joanie.Address) => {
140
+ setError(undefined);
141
+ selectAddress(address);
142
+ handleClose();
143
+ };
146
144
 
147
- /**
148
- * Create a new address according to form values
149
- * then update `selectedAddress` state with this new one.
150
- * If `save` checkbox input is checked, the address is persisted
151
- * otherwise it is only stored through the `selectedAddress` state.
152
- *
153
- * @param {AddressFormValues} formValues address fields to update
154
- */
155
- const handleCreate = async ({ save, ...address }: AddressFormValues) => {
156
- if (save) {
157
- await create(address, { onSuccess: handleSelect });
158
- } else {
159
- handleSelect({
160
- id: LOCAL_BILLING_ADDRESS_ID,
161
- is_main: false,
162
- ...address,
163
- });
164
- }
165
- };
145
+ /**
146
+ * Create a new address according to form values
147
+ * then update `selectedAddress` state with this new one.
148
+ * If `save` checkbox input is checked, the address is persisted
149
+ * otherwise it is only stored through the `selectedAddress` state.
150
+ *
151
+ * @param {AddressFormValues} formValues address fields to update
152
+ */
153
+ const handleCreate = async ({ save, ...address }: AddressFormValues) => {
154
+ if (save) {
155
+ await create(address, { onSuccess: handleSelect });
156
+ } else {
157
+ handleSelect({
158
+ id: LOCAL_BILLING_ADDRESS_ID,
159
+ is_main: false,
160
+ ...address,
161
+ });
162
+ }
163
+ };
166
164
 
167
- /**
168
- * Update the `editedAddress` with new values provided as argument
169
- * then clear `editedAddress` state if request succeeded.
170
- *
171
- * @param {AddressFormValues} formValues address fields to update
172
- */
173
- const handleUpdate = async ({ save, ...newAddress }: AddressFormValues) => {
174
- update(
175
- {
176
- ...editedAddress!,
177
- ...newAddress,
178
- },
179
- {
180
- onSuccess: () => setEditedAddress(undefined),
181
- },
182
- );
183
- };
165
+ /**
166
+ * Update the `editedAddress` with new values provided as argument
167
+ * then clear `editedAddress` state if request succeeded.
168
+ *
169
+ * @param {AddressFormValues} formValues address fields to update
170
+ */
171
+ const handleUpdate = async ({ save, ...newAddress }: AddressFormValues) => {
172
+ update(
173
+ {
174
+ ...editedAddress!,
175
+ ...newAddress,
176
+ },
177
+ {
178
+ onSuccess: () => setEditedAddress(undefined),
179
+ },
180
+ );
181
+ };
184
182
 
185
- useEffect(() => {
186
- setError(undefined);
187
- if (editedAddress) {
188
- document.querySelector<HTMLElement>('[name="address-form"] input')?.focus();
189
- }
190
- }, [editedAddress]);
183
+ useEffect(() => {
184
+ setError(undefined);
185
+ if (editedAddress) {
186
+ document.querySelector<HTMLElement>('[name="address-form"] input')?.focus();
187
+ }
188
+ }, [editedAddress]);
191
189
 
192
- return (
193
- <div className="AddressesManagement" ref={ref}>
194
- <Button
195
- className="AddressesManagement__closeButton"
196
- color="tertiary"
197
- size="small"
198
- onClick={handleClose}
199
- >
200
- <Icon name={IconTypeEnum.CHEVRON_LEFT_OUTLINE} className="button__icon" />
201
- <FormattedMessage {...messages.closeButton} />
202
- </Button>
203
- {error && <Banner message={error} type={BannerType.ERROR} rounded />}
204
- {addresses.items.length > 0 ? (
205
- <section className="address-registered">
206
- <header>
207
- <h2 className="h5">
208
- <FormattedMessage {...messages.registeredAddresses} />
209
- </h2>
210
- </header>
211
- <ul className="registered-addresses-list">
212
- {Children.toArray(
213
- addresses.items
214
- .sort(sortAddressByTitleAsc)
215
- .map((address) => (
216
- <RegisteredAddress
217
- address={address}
218
- edit={setEditedAddress}
219
- promote={promote}
220
- remove={remove}
221
- select={handleSelect}
222
- />
223
- )),
224
- )}
225
- </ul>
226
- </section>
227
- ) : null}
228
- <section className={`address-form ${editedAddress ? 'address-form--highlighted' : ''}`}>
190
+ return (
191
+ <div className="AddressesManagement" ref={ref}>
192
+ <Button
193
+ className="AddressesManagement__closeButton"
194
+ color="tertiary"
195
+ size="small"
196
+ onClick={handleClose}
197
+ >
198
+ <Icon name={IconTypeEnum.CHEVRON_LEFT_OUTLINE} className="button__icon" />
199
+ <FormattedMessage {...messages.closeButton} />
200
+ </Button>
201
+ {error && <Banner message={error} type={BannerType.ERROR} rounded />}
202
+ {addresses.items.length > 0 ? (
203
+ <section className="address-registered">
229
204
  <header>
230
205
  <h2 className="h5">
231
- {editedAddress ? (
232
- <FormattedMessage
233
- {...messages.editAddress}
234
- values={{ title: editedAddress.title }}
235
- />
236
- ) : (
237
- <FormattedMessage {...messages.addAddress} />
238
- )}
206
+ <FormattedMessage {...messages.registeredAddresses} />
239
207
  </h2>
240
208
  </header>
241
- <AddressForm
242
- address={editedAddress}
243
- handleReset={() => setEditedAddress(undefined)}
244
- onSubmit={editedAddress ? handleUpdate : handleCreate}
245
- />
209
+ <ul className="registered-addresses-list">
210
+ {Children.toArray(
211
+ addresses.items
212
+ .sort(sortAddressByTitleAsc)
213
+ .map((address) => (
214
+ <RegisteredAddress
215
+ address={address}
216
+ edit={setEditedAddress}
217
+ promote={promote}
218
+ remove={remove}
219
+ select={handleSelect}
220
+ />
221
+ )),
222
+ )}
223
+ </ul>
246
224
  </section>
247
- </div>
248
- );
249
- },
250
- );
225
+ ) : null}
226
+ <section className={`address-form ${editedAddress ? 'address-form--highlighted' : ''}`}>
227
+ <header>
228
+ <h2 className="h5">
229
+ {editedAddress ? (
230
+ <FormattedMessage {...messages.editAddress} values={{ title: editedAddress.title }} />
231
+ ) : (
232
+ <FormattedMessage {...messages.addAddress} />
233
+ )}
234
+ </h2>
235
+ </header>
236
+ <AddressForm
237
+ address={editedAddress}
238
+ handleReset={() => setEditedAddress(undefined)}
239
+ onSubmit={editedAddress ? handleUpdate : handleCreate}
240
+ />
241
+ </section>
242
+ </div>
243
+ );
244
+ };
251
245
 
252
246
  export default AddressesManagement;
@@ -134,7 +134,7 @@ const ContractFrameContent = ({
134
134
  const [signatureType, setSignatureType] = useState<SignatureType>();
135
135
  const [invitationLink, setInvitationLink] = useState<Maybe<string>>();
136
136
  const [error, setError] = useState<Maybe<string>>();
137
- const timeoutRef = useRef<NodeJS.Timeout>();
137
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
138
138
 
139
139
  const setErrored = (e: string) => {
140
140
  setStep(ContractSteps.ERROR);
@@ -29,7 +29,7 @@ type IconContainerProps = {
29
29
  };
30
30
  const IconContainer = ({ name, enumKey }: IconContainerProps) => {
31
31
  const [showTooltip, setShowTooltip] = useState(false);
32
- const timeoutRef = useRef<NodeJS.Timeout>();
32
+ const timeoutRef = useRef<NodeJS.Timeout>(undefined);
33
33
  const ENUM_NAME = 'IconTypeEnum';
34
34
 
35
35
  const styleContainer: CSSProperties = {
@@ -17,7 +17,7 @@ const PayplugLightbox = ({
17
17
  onError,
18
18
  ...props
19
19
  }: PaymentInterfaceProps<PayplugPayment>) => {
20
- const ref = useRef<ReturnType<typeof setTimeout>>();
20
+ const ref = useRef<ReturnType<typeof setTimeout>>(undefined);
21
21
 
22
22
  /** type guard to check if the payment is a payment one click */
23
23
  const isPaidPayment = (p: PayplugPayment) => p?.is_paid === true;
@@ -22,9 +22,10 @@
22
22
  font-weight: var(--c--theme--font--weight--semibold);
23
23
  font-size: rem-calc(12px);
24
24
 
25
- &--incoming {
26
- background-color: var(--c--theme--colors--grey87);
27
- color: var(--c--theme--colors--charcoal);
25
+ &--canceled,
26
+ &--refunded {
27
+ color: var(--c--theme--colors--white);
28
+ background-color: var(--c--theme--colors--grey59);
28
29
  }
29
30
 
30
31
  &--paid {
@@ -37,8 +38,8 @@
37
38
  color: var(--c--theme--colors--white);
38
39
  }
39
40
 
40
- &--require_payment,
41
- &--failed {
41
+ &--refused,
42
+ &--error {
42
43
  background-color: var(--c--theme--colors--firebrick6);
43
44
  color: var(--c--theme--colors--white);
44
45
  }
@@ -22,6 +22,12 @@ export const stateMessages = defineMessages({
22
22
  defaultMessage: 'Pending',
23
23
  description: 'Label displayed for pending payment state',
24
24
  },
25
+ [PaymentScheduleState.ERROR]: {
26
+ id: 'components.PaymentScheduleGrid.state.error',
27
+ defaultMessage: 'Pending',
28
+ description:
29
+ 'Label displayed for error payment state. For learner we assume to display `pending`.',
30
+ },
25
31
  [PaymentScheduleState.PAID]: {
26
32
  id: 'components.PaymentScheduleGrid.state.paid',
27
33
  defaultMessage: 'Paid',
@@ -32,6 +38,16 @@ export const stateMessages = defineMessages({
32
38
  defaultMessage: 'Refused',
33
39
  description: 'Label displayed for refused payment state',
34
40
  },
41
+ [PaymentScheduleState.CANCELED]: {
42
+ id: 'components.PaymentScheduleGrid.state.canceled',
43
+ defaultMessage: 'Canceled',
44
+ description: 'Label displayed for canceled payment state',
45
+ },
46
+ [PaymentScheduleState.REFUNDED]: {
47
+ id: 'components.PaymentScheduleGrid.state.refunded',
48
+ defaultMessage: 'Refunded',
49
+ description: 'Label displayed for refunded payment state',
50
+ },
35
51
  });
36
52
 
37
53
  export const PaymentScheduleGrid = ({ schedule }: Props) => {
@@ -47,7 +47,7 @@ const messages = defineMessages({
47
47
  });
48
48
 
49
49
  const SaleTunnelSavePaymentMethod = () => {
50
- const initialCreditCards = useRef<CreditCard[]>();
50
+ const initialCreditCards = useRef<CreditCard[]>([]);
51
51
  const [shouldPoll, setShouldPoll] = useState(false);
52
52
  const [payment, setPayment] = useState<Payment>();
53
53
  const [error, setError] = useState<string>();
@@ -71,7 +71,7 @@ const SaleTunnelSavePaymentMethod = () => {
71
71
  };
72
72
 
73
73
  const waitForNewCreditCard = () => {
74
- const initialIds = initialCreditCards.current!.map((cc) => cc.id);
74
+ const initialIds = initialCreditCards.current.map((cc) => cc.id);
75
75
  const newCard = creditCardsQuery.items.find((cc) => !initialIds.includes(cc.id));
76
76
 
77
77
  if (!newCard) return;
@@ -10,14 +10,15 @@ describe('<SearchInput />', () => {
10
10
 
11
11
  const inputProps = {};
12
12
 
13
- it('renders with the input field and button', () => {
14
- const { container, getByText } = render(
13
+ it('renders with the input field, label and button', () => {
14
+ const { container, getByRole, getByLabelText } = render(
15
15
  <IntlProvider locale="en">
16
16
  <SearchInput context={contextProps} inputProps={inputProps} />
17
17
  </IntlProvider>,
18
18
  );
19
19
 
20
- getByText('Search');
20
+ getByRole('button', { name: 'Search' });
21
+ getByLabelText('Search');
21
22
  // NB: we're searching the DOM as labelling and a11y for this input are handled by `react-autosuggest`
22
23
  expect(container.querySelector('input')).not.toBeNull();
23
24
  expect(container.innerHTML).toContain('icon-magnifying-glass');
@@ -26,13 +27,13 @@ describe('<SearchInput />', () => {
26
27
  it('triggers the passed callback on click', () => {
27
28
  const callback = jest.fn();
28
29
 
29
- const { getByText } = render(
30
+ const { getByRole } = render(
30
31
  <IntlProvider locale="en">
31
32
  <SearchInput context={contextProps} inputProps={inputProps} onClick={callback} />
32
33
  </IntlProvider>,
33
34
  );
34
35
 
35
- fireEvent.click(getByText('Search'));
36
+ fireEvent.click(getByRole('button', { name: 'Search' }));
36
37
  expect(callback).toHaveBeenCalledTimes(1);
37
38
  });
38
39
  });