richie-education 2.34.1-dev4 → 2.34.1-dev45

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.
@@ -1,4 +1,4 @@
1
- import { ButtonElement, Input } from '@openfun/cunningham-react';
1
+ import { ButtonElement, Input, Alert, VariantType } from '@openfun/cunningham-react';
2
2
  import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
3
3
  import { FormProvider, useForm } from 'react-hook-form';
4
4
  import * as Yup from 'yup';
@@ -8,7 +8,6 @@ import { useSession } from 'contexts/SessionContext';
8
8
  import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
9
9
  import Form, { getLocalizedCunninghamErrorProp } from 'components/Form';
10
10
  import { Spinner } from 'components/Spinner';
11
- import Banner, { BannerType } from 'components/Banner';
12
11
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
13
12
 
14
13
  const messages = defineMessages({
@@ -19,14 +18,15 @@ const messages = defineMessages({
19
18
  },
20
19
  fullNameInputLabel: {
21
20
  id: 'components.OpenEdxFullNameForm.fullNameInputLabel',
22
- description: 'Label of "fullName" field of the openEdx full name form',
23
- defaultMessage: 'Full name',
21
+ description: 'Label of "First name and last name" field of the openEdx full name form',
22
+ defaultMessage: 'First name and last name',
24
23
  },
25
24
  fullNameInputDescription: {
26
25
  id: 'components.OpenEdxFullNameForm.fullNameInputDescription',
27
- description: 'Descripiton on the "fullName" field of the openEdx full name form.',
26
+ description:
27
+ 'Description about the "First name and last name" field of the openEdx full name form.',
28
28
  defaultMessage:
29
- 'Please check that your fullname is correct. It will be used on official document (e.g: certificate)',
29
+ 'Please check that your first name and last name are correct. They will be used on official document (e.g: certificate, contract, etc.)',
30
30
  },
31
31
  submitButtonLabel: {
32
32
  id: 'components.OpenEdxFullNameForm.submitButtonLabel',
@@ -128,27 +128,24 @@ const OpenEdxFullNameForm = () => {
128
128
 
129
129
  if (error) {
130
130
  // display get error message
131
- return <Banner type={BannerType.ERROR} message={error} />;
131
+ return <Alert type={VariantType.ERROR}>{error}</Alert>;
132
132
  }
133
133
 
134
134
  return (
135
135
  <FormProvider {...form}>
136
136
  <Form name="openedx-fullname-form" noValidate>
137
+ <Alert type={formState.errors.name?.message ? VariantType.ERROR : VariantType.WARNING}>
138
+ <FormattedMessage {...messages.fullNameInputDescription} />
139
+ </Alert>
137
140
  <Input
138
141
  {...register('name')}
139
- className="form-field"
142
+ className="form-field mt-s"
140
143
  required
141
144
  fullWidth
142
145
  label={intl.formatMessage(messages.fullNameInputLabel)}
143
146
  value={formState.defaultValues?.name}
144
- state={
145
- error || (formState.errors.name && formState.errors.name.message) ? 'error' : 'default'
146
- }
147
- text={
148
- error ||
149
- getLocalizedCunninghamErrorProp(intl, formState.errors.name?.message).text ||
150
- intl.formatMessage(messages.fullNameInputDescription)
151
- }
147
+ state={error || formState.errors.name?.message ? 'error' : 'default'}
148
+ text={error || getLocalizedCunninghamErrorProp(intl, formState.errors.name?.message).text}
152
149
  />
153
150
  </Form>
154
151
  </FormProvider>
@@ -184,7 +184,7 @@ describe('SaleTunnel', () => {
184
184
  */
185
185
  screen.getByText('Information');
186
186
 
187
- const nameInput = screen.getByLabelText('Full name');
187
+ const nameInput = screen.getByLabelText('First name and last name');
188
188
  await user.type(nameInput, 'John Doe');
189
189
 
190
190
  /**
@@ -3,6 +3,19 @@ import { Resource } from 'types/Resource';
3
3
  import { Nullable } from 'types/utils';
4
4
  import { CourseListItem as JoanieCourse } from 'types/Joanie';
5
5
 
6
+ export enum CourseOffer {
7
+ PAID = 'paid',
8
+ FREE = 'free',
9
+ PARTIALLY_FREE = 'partially_free',
10
+ SUBSCRIPTION = 'subscription',
11
+ }
12
+
13
+ export enum CourseCertificateOffer {
14
+ PAID = 'paid',
15
+ FREE = 'free',
16
+ SUBSCRIPTION = 'subscription',
17
+ }
18
+
6
19
  export interface Course extends Resource {
7
20
  absolute_url: string;
8
21
  categories: string[];
@@ -29,6 +42,11 @@ export interface Course extends Resource {
29
42
  }>;
30
43
  organizations: string[];
31
44
  state: CourseState;
45
+ certificate_offer: Nullable<CourseCertificateOffer>;
46
+ offer: Nullable<CourseOffer>;
47
+ certificate_price: Nullable<number>;
48
+ price: Nullable<number>;
49
+ price_currency: string;
32
50
  }
33
51
 
34
52
  export function isRichieCourse(course: Course | JoanieCourse): course is Course {
package/js/types/index.ts CHANGED
@@ -35,6 +35,11 @@ export interface CourseRun {
35
35
  title?: string;
36
36
  snapshot?: string;
37
37
  display_mode: CourseRunDisplayMode;
38
+ price?: number;
39
+ price_currency?: string;
40
+ offer?: string;
41
+ certificate_price?: number;
42
+ certificate_offer?: string;
38
43
  }
39
44
 
40
45
  export enum Priority {
@@ -0,0 +1,63 @@
1
+ import { waitFor } from '@testing-library/react';
2
+ import { VariantType } from '@openfun/cunningham-react';
3
+
4
+ export const expectAlertError = (message: string, rootElement: ParentNode = document) => {
5
+ return expectAlert(VariantType.ERROR, message, rootElement);
6
+ };
7
+
8
+ export const expectAlertInfo = (message: string, rootElement: ParentNode = document) => {
9
+ return expectAlert(VariantType.INFO, message, rootElement);
10
+ };
11
+
12
+ export const expectAlertSuccess = (message: string, rootElement: ParentNode = document) => {
13
+ return expectAlert(VariantType.SUCCESS, message, rootElement);
14
+ };
15
+
16
+ export const expectAlertWarning = (message: string, rootElement: ParentNode = document) => {
17
+ return expectAlert(VariantType.WARNING, message, rootElement);
18
+ };
19
+
20
+ export const expectAlert = (
21
+ type: VariantType,
22
+ message: string,
23
+ rootElement: ParentNode = document,
24
+ ) => {
25
+ return waitFor(async () => {
26
+ // Cunningham Alert has a class with the alert variant type
27
+ const alert = rootElement.querySelector(`.c__alert--${type}`) as HTMLElement;
28
+ expect(alert).not.toBeNull();
29
+ expect(alert).toHaveTextContent(message);
30
+ });
31
+ };
32
+
33
+ export const expectNoAlert = (
34
+ type: VariantType,
35
+ message: string,
36
+ rootElement: ParentNode = document,
37
+ ) => {
38
+ return waitFor(() => {
39
+ // Check that no alert exists with this message and type
40
+ const alerts = rootElement.querySelectorAll('.c__alert');
41
+ const matchingAlert = Array.from(alerts).find(
42
+ (alert) =>
43
+ alert.classList.contains(`c__alert--${type}`) && alert.textContent?.includes(message),
44
+ );
45
+ expect(matchingAlert).toBeUndefined();
46
+ });
47
+ };
48
+
49
+ export const expectNoAlertError = (message: string, rootElement: ParentNode = document) => {
50
+ return expectNoAlert(VariantType.ERROR, message, rootElement);
51
+ };
52
+
53
+ export const expectNoAlertInfo = (message: string, rootElement: ParentNode = document) => {
54
+ return expectNoAlert(VariantType.INFO, message, rootElement);
55
+ };
56
+
57
+ export const expectNoAlertSuccess = (message: string, rootElement: ParentNode = document) => {
58
+ return expectNoAlert(VariantType.SUCCESS, message, rootElement);
59
+ };
60
+
61
+ export const expectNoAlertWarning = (message: string, rootElement: ParentNode = document) => {
62
+ return expectNoAlert(VariantType.WARNING, message, rootElement);
63
+ };
@@ -10,7 +10,7 @@ import {
10
10
  PacedCourse,
11
11
  Priority,
12
12
  } from 'types';
13
- import { Course } from 'types/Course';
13
+ import { CourseCertificateOffer, Course, CourseOffer } from 'types/Course';
14
14
  import { FactoryHelper } from 'utils/test/factories/helper';
15
15
  import { factory } from './factories';
16
16
 
@@ -45,7 +45,27 @@ export const CourseStateFutureOpenFactory = factory<CourseState>(() => {
45
45
  };
46
46
  });
47
47
 
48
+ enum OfferType {
49
+ PAID = 'PAID',
50
+ FREE = 'FREE',
51
+ PARTIALLY_FREE = 'PARTIALLY_FREE',
52
+ SUBSCRIPTION = 'SUBSCRIPTION',
53
+ }
54
+
48
55
  export const CourseRunFactory = factory<CourseRun>(() => {
56
+ const offerValues = Object.values(OfferType);
57
+ const offer = offerValues[Math.floor(Math.random() * offerValues.length)];
58
+ const certificateOfferValues = [OfferType.PAID, OfferType.FREE, OfferType.SUBSCRIPTION];
59
+ const certificateOffer =
60
+ certificateOfferValues[Math.floor(Math.random() * certificateOfferValues.length)];
61
+ const currency = faker.finance.currency().code;
62
+ const price = [OfferType.FREE, OfferType.PARTIALLY_FREE].includes(offer)
63
+ ? 0
64
+ : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
65
+ const certificatePrice =
66
+ certificateOffer === OfferType.FREE
67
+ ? 0
68
+ : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
49
69
  return {
50
70
  id: faker.number.int(),
51
71
  resource_link: FactoryHelper.unique(faker.internet.url),
@@ -58,6 +78,11 @@ export const CourseRunFactory = factory<CourseRun>(() => {
58
78
  dashboard_link: null,
59
79
  title: faker.lorem.sentence(3),
60
80
  display_mode: CourseRunDisplayMode.DETAILED,
81
+ price,
82
+ price_currency: currency,
83
+ offer,
84
+ certificate_price: certificatePrice,
85
+ certificate_offer: certificateOffer,
61
86
  };
62
87
  });
63
88
 
@@ -214,5 +239,10 @@ export const CourseLightFactory = factory<Course>(() => {
214
239
  },
215
240
  organizations: [organizationName],
216
241
  state: CourseStateFactory().one(),
242
+ certificate_offer: CourseCertificateOffer.FREE,
243
+ offer: CourseOffer.FREE,
244
+ certificate_price: null,
245
+ price: null,
246
+ price_currency: 'EUR',
217
247
  };
218
248
  });
@@ -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} />