richie-education 2.33.1-dev9 → 2.34.1-dev10

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 (70) 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.spec.tsx +2 -4
  15. package/js/components/ContractFrame/AbstractContractFrame.tsx +1 -1
  16. package/js/components/Icon/index.stories.tsx +1 -1
  17. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  18. package/js/components/PaymentScheduleGrid/_styles.scss +6 -5
  19. package/js/components/PaymentScheduleGrid/index.tsx +16 -0
  20. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +2 -2
  21. package/js/components/SearchInput/index.spec.tsx +6 -5
  22. package/js/components/SearchInput/index.tsx +9 -1
  23. package/js/hooks/useCreditCards/index.spec.tsx +84 -0
  24. package/js/hooks/useCreditCards/index.ts +53 -1
  25. package/js/hooks/useCreditCardsManagement.tsx +1 -10
  26. package/js/hooks/useLearnerCoursesSearch/index.tsx +2 -2
  27. package/js/pages/DashboardCourses/index.spec.tsx +51 -7
  28. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -5
  29. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +1 -1
  30. package/js/pages/DashboardCreditCardsManagement/_styles.scss +11 -3
  31. package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +46 -25
  32. package/js/pages/DashboardCreditCardsManagement/index.tsx +21 -37
  33. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -2
  34. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +1 -1
  35. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +8 -5
  36. package/js/translations/ar-SA.json +1 -1
  37. package/js/translations/es-ES.json +1 -1
  38. package/js/translations/fa-IR.json +1 -1
  39. package/js/translations/fr-CA.json +1 -1
  40. package/js/translations/fr-FR.json +1 -1
  41. package/js/translations/ko-KR.json +1 -1
  42. package/js/translations/pt-PT.json +1 -1
  43. package/js/translations/ru-RU.json +1 -1
  44. package/js/translations/vi-VN.json +1 -1
  45. package/js/types/Joanie.ts +13 -1
  46. package/js/types/index.ts +5 -0
  47. package/js/utils/OrderHelper/index.ts +9 -0
  48. package/js/utils/errors/HttpError.ts +1 -0
  49. package/js/utils/test/factories/richie.ts +25 -0
  50. package/js/utils/test/wrappers/types.ts +2 -2
  51. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +7 -1
  52. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +14 -2
  53. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +3 -2
  54. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +12 -5
  55. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +1 -1
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +5 -0
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +10 -8
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +5 -0
  59. package/js/widgets/Dashboard/components/SearchBar/index.spec.tsx +1 -1
  60. package/js/widgets/Dashboard/components/SearchResultsCount/index.spec.tsx +1 -1
  61. package/js/widgets/Dashboard/index.spec.tsx +7 -1
  62. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -1
  63. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +6 -4
  64. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +1 -5
  65. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +107 -0
  66. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +107 -0
  67. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +450 -5
  68. package/package.json +66 -64
  69. package/scss/components/_styleguide.scss +2 -1
  70. package/scss/vendors/css/cunningham-tokens.css +1 -0
@@ -4,7 +4,7 @@ import { render } from 'utils/test/render';
4
4
  import { PresentationalAppWrapper } from 'utils/test/wrappers/PresentationalAppWrapper';
5
5
  import SearchBar from '.';
6
6
 
7
- describe('Dashbaord/components/SearchBar', () => {
7
+ describe('Dashboard/components/SearchBar', () => {
8
8
  it('should render', () => {
9
9
  render(<SearchBar onSubmit={jest.fn()} />, { wrapper: PresentationalAppWrapper });
10
10
  expect(screen.getByRole('textbox', { name: /Search/ })).toBeInTheDocument();
@@ -3,7 +3,7 @@ import { render } from 'utils/test/render';
3
3
  import { PresentationalAppWrapper } from 'utils/test/wrappers/PresentationalAppWrapper';
4
4
  import SearchResultsCount from '.';
5
5
 
6
- describe('Dashbaord/components/SearchResultsCount', () => {
6
+ describe('Dashboard/components/SearchResultsCount', () => {
7
7
  it('should render singular message', () => {
8
8
  render(<SearchResultsCount nbResults={1} />, {
9
9
  wrapper: PresentationalAppWrapper,
@@ -49,7 +49,13 @@ describe('<Dashboard />', () => {
49
49
  { count: 0, results: [] },
50
50
  );
51
51
  fetchMock.get(
52
- 'https://joanie.endpoint/api/v1.0/orders/?product_type=credential&state_exclude=canceled&page=1&page_size=50',
52
+ 'https://joanie.endpoint/api/v1.0/orders/' +
53
+ '?product_type=credential' +
54
+ '&state_exclude=canceled' +
55
+ '&state_exclude=refunding' +
56
+ '&state_exclude=refunded' +
57
+ '&page=1' +
58
+ '&page_size=50',
53
59
  {
54
60
  count: 0,
55
61
  results: [],
@@ -139,11 +139,12 @@ const EnrollableCourseRunList = ({ courseRuns, order }: Props) => {
139
139
  <ol className="course-runs-list">
140
140
  {Children.toArray(
141
141
  courseRuns.map((courseRun) => (
142
- <li className="course-runs-item form-field">
142
+ <li key={`${order.id}|${courseRun.id}`} className="course-runs-item form-field">
143
143
  <input
144
144
  className="form-field__radio-input"
145
145
  type="radio"
146
146
  id={`${order.id}|${courseRun.id}`}
147
+ data-testid={`radio-input-${order.id}-${courseRun.id}`}
147
148
  name={order.id}
148
149
  disabled={needsSignature}
149
150
  aria-label={intl.formatMessage(messages.ariaSelectCourseRun, {
@@ -166,11 +166,13 @@ describe('CourseProductCourseRuns', () => {
166
166
  );
167
167
 
168
168
  // - A radio input
169
- screen.getByRole('radio', {
170
- name: `Select course run from ${dateFormatter.format(
169
+ const $input = screen.getByTestId(`radio-input-${order.id}-${courseRun.id}`);
170
+ expect($input).toHaveAttribute('type', 'radio');
171
+ expect($input).toHaveAccessibleName(
172
+ `Select course run from ${dateFormatter.format(
171
173
  new Date(courseRun.start),
172
174
  )} to ${dateFormatter.format(new Date(courseRun.end))}.`,
173
- });
175
+ );
174
176
  });
175
177
 
176
178
  // A call to action should be displayed
@@ -215,7 +217,7 @@ describe('CourseProductCourseRuns', () => {
215
217
  });
216
218
 
217
219
  // A spinner should be displayed
218
- screen.getByRole('status', { name: 'Enrolling...' });
220
+ await screen.findByRole('status', { name: 'Enrolling...' });
219
221
 
220
222
  await act(async () => {
221
223
  enrollmentDeferred.resolve(HttpStatusCode.OK);
@@ -195,11 +195,7 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
195
195
  step,
196
196
  },
197
197
  dispatch,
198
- ] = useReducer<React.Reducer<ReducerState, ReducerAction>, ReducerState>(
199
- reducer,
200
- initialState(user, props.courseRun, enrollmentIsActive),
201
- (s) => s,
202
- );
198
+ ] = useReducer(reducer, initialState(user, props.courseRun, enrollmentIsActive));
203
199
 
204
200
  const setEnroll = useCallback(
205
201
  async (isActive: boolean = true) => {
@@ -46,6 +46,51 @@ const messages = defineMessages({
46
46
  description: 'Course date of an opened course run block',
47
47
  defaultMessage: 'From {startDate} {endDate, select, undefined {} other {to {endDate}}}',
48
48
  },
49
+ coursePrice: {
50
+ id: 'components.SyllabusCourseRun.coursePrice',
51
+ description: 'Title of the course enrollment price section of an opened course run block',
52
+ defaultMessage: 'Enrollment price',
53
+ },
54
+ certificationPrice: {
55
+ id: 'components.SyllabusCourseRun.certificationPrice',
56
+ description: 'Title of the certification price section of an opened course run block',
57
+ defaultMessage: 'Certification price',
58
+ },
59
+ coursePaidOffer: {
60
+ id: 'components.SyllabusCourseRun.coursePaidOffer',
61
+ description: 'Message for the paid course offer of an opened course run block',
62
+ defaultMessage: 'The course content is paid.',
63
+ },
64
+ courseFreeOffer: {
65
+ id: 'components.SyllabusCourseRun.courseFreeOffer',
66
+ description: 'Message for the free course offer of an opened course run block',
67
+ defaultMessage: 'The course content is free.',
68
+ },
69
+ coursePartiallyFree: {
70
+ id: 'components.SyllabusCourseRun.coursePartiallyFree',
71
+ description: 'Message for the partially free course offer of an opened course run block',
72
+ defaultMessage: 'The course content is free.',
73
+ },
74
+ courseSubscriptionOffer: {
75
+ id: 'components.SyllabusCourseRun.courseSubscriptionOffer',
76
+ description: 'Message for the subscription course offer of an opened course run block',
77
+ defaultMessage: 'Subscribe to access the course content.',
78
+ },
79
+ certificatePaidOffer: {
80
+ id: 'components.SyllabusCourseRun.certificatePaidOffer',
81
+ description: 'Messagge for the paid certification offer of an opened course run block',
82
+ defaultMessage: 'The certification process is paid.',
83
+ },
84
+ certificateFreeOffer: {
85
+ id: 'components.SyllabusCourseRun.certificateFreeOffer',
86
+ description: 'Message for the free certification offer of an opened course run block',
87
+ defaultMessage: 'The certification process is free.',
88
+ },
89
+ certificateSubscriptionOffer: {
90
+ id: 'components.SyllabusCourseRun.certificateSubscriptionOffer',
91
+ description: 'Message for the subscription certification offer of an opened course run block',
92
+ defaultMessage: 'The certification process is offered through subscription.',
93
+ },
49
94
  });
50
95
 
51
96
  const OpenedCourseRun = ({
@@ -63,6 +108,44 @@ const OpenedCourseRun = ({
63
108
  const enrollmentEnd = courseRun.enrollment_end ? formatDate(courseRun.enrollment_end) : '...';
64
109
  const start = courseRun.start ? formatDate(courseRun.start) : '...';
65
110
  const end = courseRun.end ? formatDate(courseRun.end) : '...';
111
+ let courseOfferMessage = null;
112
+ let certificationOfferMessage = null;
113
+ let enrollmentPrice = '';
114
+ let certificatePrice = '';
115
+
116
+ if (courseRun.offer) {
117
+ const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
118
+ courseOfferMessage = {
119
+ PAID: messages.coursePaidOffer,
120
+ FREE: messages.courseFreeOffer,
121
+ PARTIALLY_FREE: messages.coursePartiallyFree,
122
+ SUBSCRIPTION: messages.courseSubscriptionOffer,
123
+ }[offer];
124
+
125
+ if ((courseRun.price ?? -1) >= 0) {
126
+ enrollmentPrice = intl.formatNumber(courseRun.price!, {
127
+ style: 'currency',
128
+ currency: courseRun.price_currency,
129
+ });
130
+ }
131
+ }
132
+
133
+ if (courseRun.certificate_offer) {
134
+ const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
135
+ certificationOfferMessage = {
136
+ PAID: messages.certificatePaidOffer,
137
+ FREE: messages.certificateFreeOffer,
138
+ SUBSCRIPTION: messages.certificateSubscriptionOffer,
139
+ }[certificationOffer];
140
+
141
+ if ((courseRun.certificate_price ?? -1) >= 0) {
142
+ certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
143
+ style: 'currency',
144
+ currency: courseRun.price_currency,
145
+ });
146
+ }
147
+ }
148
+
66
149
  return (
67
150
  <>
68
151
  {courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
@@ -99,6 +182,30 @@ const OpenedCourseRun = ({
99
182
  <dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
100
183
  </>
101
184
  )}
185
+ {courseOfferMessage && (
186
+ <>
187
+ <dt>
188
+ <FormattedMessage {...messages.coursePrice} />
189
+ </dt>
190
+ <dd>
191
+ <FormattedMessage {...courseOfferMessage} />
192
+ <br />
193
+ {enrollmentPrice}
194
+ </dd>
195
+ </>
196
+ )}
197
+ {certificationOfferMessage && (
198
+ <>
199
+ <dt>
200
+ <FormattedMessage {...messages.certificationPrice} />
201
+ </dt>
202
+ <dd>
203
+ <FormattedMessage {...certificationOfferMessage} />
204
+ <br />
205
+ {certificatePrice}
206
+ </dd>
207
+ </>
208
+ )}
102
209
  </dl>
103
210
  {findLmsBackend(courseRun.resource_link) ? (
104
211
  <CourseRunEnrollment courseRun={courseRun} />
@@ -41,6 +41,51 @@ const messages = defineMessages({
41
41
  description: 'Self paced course run block with no end date',
42
42
  defaultMessage: 'Available',
43
43
  },
44
+ coursePrice: {
45
+ id: 'components.SyllabusCourseRunCompacted.coursePrice',
46
+ description: 'Title of the course enrollment price section of an opened course run block',
47
+ defaultMessage: 'Enrollment price',
48
+ },
49
+ certificationPrice: {
50
+ id: 'components.SyllabusCourseRunCompacted.certificationPrice',
51
+ description: 'Title of the certification price section of an opened course run block',
52
+ defaultMessage: 'Certification price',
53
+ },
54
+ coursePaidOffer: {
55
+ id: 'components.SyllabusCourseRunCompacted.coursePaidOffer',
56
+ description: 'Message for the paid course offer of an opened course run block',
57
+ defaultMessage: 'The course content is paid.',
58
+ },
59
+ courseFreeOffer: {
60
+ id: 'components.SyllabusCourseRunCompacted.courseFreeOffer',
61
+ description: 'Message for the free course offer of an opened course run block',
62
+ defaultMessage: 'The course content is free.',
63
+ },
64
+ coursePartiallyFree: {
65
+ id: 'components.SyllabusCourseRunCompacted.coursePartiallyFree',
66
+ description: 'Message for the partially free course offer of an opened course run block',
67
+ defaultMessage: 'The course content is free.',
68
+ },
69
+ courseSubscriptionOffer: {
70
+ id: 'components.SyllabusCourseRunCompacted.courseSubscriptionOffer',
71
+ description: 'Message for the subscription course offer of an opened course run block',
72
+ defaultMessage: 'Subscribe to access the course content.',
73
+ },
74
+ certificatePaidOffer: {
75
+ id: 'components.SyllabusCourseRunCompacted.certificatePaidOffer',
76
+ description: 'Messagge for the paid certification offer of an opened course run block',
77
+ defaultMessage: 'The certification process is paid.',
78
+ },
79
+ certificateFreeOffer: {
80
+ id: 'components.SyllabusCourseRunCompacted.certificateFreeOffer',
81
+ description: 'Message for the free certification offer of an opened course run block',
82
+ defaultMessage: 'The certification process is free.',
83
+ },
84
+ certificateSubscriptionOffer: {
85
+ id: 'components.SyllabusCourseRunCompacted.certificateSubscriptionOffer',
86
+ description: 'Message for the subscription certification offer of an opened course run block',
87
+ defaultMessage: 'The certification process is offered through subscription.',
88
+ },
44
89
  });
45
90
 
46
91
  const OpenedSelfPacedCourseRun = ({
@@ -54,6 +99,44 @@ const OpenedSelfPacedCourseRun = ({
54
99
  const intl = useIntl();
55
100
  const end = courseRun.end ? formatDate(courseRun.end) : '...';
56
101
  const hasEndDate = end !== '...';
102
+ let courseOfferMessage = null;
103
+ let certificationOfferMessage = null;
104
+ let enrollmentPrice = '';
105
+ let certificatePrice = '';
106
+
107
+ if (courseRun.offer) {
108
+ const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
109
+ courseOfferMessage = {
110
+ PAID: messages.coursePaidOffer,
111
+ FREE: messages.courseFreeOffer,
112
+ PARTIALLY_FREE: messages.coursePartiallyFree,
113
+ SUBSCRIPTION: messages.courseSubscriptionOffer,
114
+ }[offer];
115
+
116
+ if ((courseRun.price ?? -1) >= 0) {
117
+ enrollmentPrice = intl.formatNumber(courseRun.price!, {
118
+ style: 'currency',
119
+ currency: courseRun.price_currency,
120
+ });
121
+ }
122
+ }
123
+
124
+ if (courseRun.certificate_offer) {
125
+ const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
126
+ certificationOfferMessage = {
127
+ PAID: messages.certificatePaidOffer,
128
+ FREE: messages.certificateFreeOffer,
129
+ SUBSCRIPTION: messages.certificateSubscriptionOffer,
130
+ }[certificationOffer];
131
+
132
+ if ((courseRun.certificate_price ?? -1) >= 0) {
133
+ certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
134
+ style: 'currency',
135
+ currency: courseRun.price_currency,
136
+ });
137
+ }
138
+ }
139
+
57
140
  return (
58
141
  <>
59
142
  {courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
@@ -83,6 +166,30 @@ const OpenedSelfPacedCourseRun = ({
83
166
  <dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
84
167
  </>
85
168
  )}
169
+ {courseOfferMessage && (
170
+ <>
171
+ <dt>
172
+ <FormattedMessage {...messages.coursePrice} />
173
+ </dt>
174
+ <dd>
175
+ <FormattedMessage {...courseOfferMessage} />
176
+ <br />
177
+ {enrollmentPrice}
178
+ </dd>
179
+ </>
180
+ )}
181
+ {certificationOfferMessage && (
182
+ <>
183
+ <dt>
184
+ <FormattedMessage {...messages.certificationPrice} />
185
+ </dt>
186
+ <dd>
187
+ <FormattedMessage {...certificationOfferMessage} />
188
+ <br />
189
+ {certificatePrice}
190
+ </dd>
191
+ </>
192
+ )}
86
193
  </dl>
87
194
  {findLmsBackend(courseRun.resource_link) ? (
88
195
  <CourseRunEnrollment courseRun={courseRun} />