richie-education 2.30.1-dev9 → 2.31.1-dev4

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 (55) hide show
  1. package/i18n/locales/ar-SA.json +32 -0
  2. package/i18n/locales/es-ES.json +32 -0
  3. package/i18n/locales/fa-IR.json +32 -0
  4. package/i18n/locales/fr-CA.json +32 -0
  5. package/i18n/locales/fr-FR.json +32 -0
  6. package/i18n/locales/ko-KR.json +32 -0
  7. package/i18n/locales/pt-PT.json +125 -93
  8. package/i18n/locales/ru-RU.json +32 -0
  9. package/i18n/locales/vi-VN.json +32 -0
  10. package/js/api/lms/openedx-fonzie.spec.ts +1 -1
  11. package/js/api/lms/openedx-hawthorn.spec.ts +1 -1
  12. package/js/components/CourseGlimpse/utils.ts +3 -3
  13. package/js/components/CourseGlimpseList/utils.ts +2 -2
  14. package/js/components/PaymentInterfaces/types.ts +1 -0
  15. package/js/components/PurchaseButton/index.spec.tsx +20 -2
  16. package/js/components/PurchaseButton/index.tsx +3 -0
  17. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -0
  18. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -1
  19. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -0
  20. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +8 -0
  21. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +17 -2
  22. package/js/components/SaleTunnel/WithdrawRightCheckbox/index.tsx +105 -0
  23. package/js/components/SaleTunnel/index.credential.spec.tsx +2 -2
  24. package/js/components/SaleTunnel/index.full-process.spec.tsx +22 -2
  25. package/js/components/SaleTunnel/index.spec.tsx +83 -6
  26. package/js/components/SaleTunnel/index.stories.tsx +1 -0
  27. package/js/components/SaleTunnel/index.tsx +1 -1
  28. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  29. package/js/contexts/SessionContext/index.spec.tsx +2 -2
  30. package/js/hooks/useCourseProductUnion/index.ts +2 -1
  31. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  32. package/js/translations/ar-SA.json +1 -1
  33. package/js/translations/es-ES.json +1 -1
  34. package/js/translations/fa-IR.json +1 -1
  35. package/js/translations/fr-CA.json +1 -1
  36. package/js/translations/fr-FR.json +1 -1
  37. package/js/translations/ko-KR.json +1 -1
  38. package/js/translations/pt-PT.json +1 -1
  39. package/js/translations/ru-RU.json +1 -1
  40. package/js/translations/vi-VN.json +1 -1
  41. package/js/types/Joanie.ts +9 -4
  42. package/js/utils/test/factories/factories.spec.ts +1 -1
  43. package/js/utils/test/factories/joanie.ts +8 -5
  44. package/js/utils/test/factories/openEdx.tsx +1 -1
  45. package/js/utils/test/factories/richie.ts +1 -1
  46. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +2 -1
  47. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +29 -5
  48. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +7 -1
  49. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +1 -0
  50. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +2 -0
  51. package/package.json +3 -2
  52. package/scss/colors/_theme.scss +4 -0
  53. package/scss/components/_index.scss +1 -0
  54. package/scss/components/templates/richie/_multiple-columns.scss +8 -5
  55. package/scss/components/templates/richie/simpletext/_simpletext.scss +43 -0
@@ -1355,6 +1355,34 @@
1355
1355
  "description": "Message explaining the subscription process with a training agreement to sign and a payment method to set.",
1356
1356
  "message": "To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method."
1357
1357
  },
1358
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate .waiveCheckboxHelperClause2": {
1359
+ "description": "Second clause item for the waiver checkbox.",
1360
+ "message": "I understand that if I access the exam during this period, I expressly waive my right of withdrawal."
1361
+ },
1362
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate.waiveCheckboxHelperClause1": {
1363
+ "description": "First clause item for the waiver checkbox.",
1364
+ "message": "I acknowledge that I have been informed of my legal right of withdrawal, which allows me to cancel my registration within 14 days from the date of payment."
1365
+ },
1366
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate.waiverLabel": {
1367
+ "description": "Text to explain why the user has to waive to its withdrawal right.",
1368
+ "message": "If you access the exam, you acknowledge waiving your 14-day withdrawal right, as provided for in Article L221-18 of the French Consumer Code."
1369
+ },
1370
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause1": {
1371
+ "description": "First clause item for the waiver checkbox.",
1372
+ "message": "I acknowledge that I have expressly requested to begin the training before the expiration date of the withdrawal period."
1373
+ },
1374
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause2": {
1375
+ "description": "Second clause item for the waiver checkbox.",
1376
+ "message": "I expressly waive my right of withdrawal in order to begin the training before the expiration of the withdrawal period."
1377
+ },
1378
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiverLabel": {
1379
+ "description": "Text to explain why the user has to waive to its withdrawal right.",
1380
+ "message": "The training program you wish to enroll in begins before the end of the 14-day withdrawal period mentioned in Article L221-18 of the French Consumer Code. You must check the box below to proceed with your registration."
1381
+ },
1382
+ "components.SaleTunnel.WithdrawRightCheckbox.waiveCheckboxLabel": {
1383
+ "description": "Label of the checkbox to waive the withdrawal right.",
1384
+ "message": "By checking this box:"
1385
+ },
1358
1386
  "components.SaleTunnel.callToActionDescription": {
1359
1387
  "description": "Additional description announced by screen readers when focusing the call to action buying button",
1360
1388
  "message": "Purchase {product}"
@@ -1535,6 +1563,10 @@
1535
1563
  "description": "Error message shown when order creation request failed because there is no remaining available seat for the product.",
1536
1564
  "message": "There are no more places available for this product."
1537
1565
  },
1566
+ "components.SubscriptionButton.errorWithdrawalRight": {
1567
+ "description": "Error message shown when the user must waive its withdrawal right but doesn't.",
1568
+ "message": "You must waive your withdrawal right."
1569
+ },
1538
1570
  "components.SubscriptionButton.orderCreationInProgress": {
1539
1571
  "description": "Label for screen reader when an order creation is in progress.",
1540
1572
  "message": "Order creation in progress"
@@ -1355,6 +1355,34 @@
1355
1355
  "description": "Message explaining the subscription process with a training agreement to sign and a payment method to set.",
1356
1356
  "message": "To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method."
1357
1357
  },
1358
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate .waiveCheckboxHelperClause2": {
1359
+ "description": "Second clause item for the waiver checkbox.",
1360
+ "message": "I understand that if I access the exam during this period, I expressly waive my right of withdrawal."
1361
+ },
1362
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate.waiveCheckboxHelperClause1": {
1363
+ "description": "First clause item for the waiver checkbox.",
1364
+ "message": "I acknowledge that I have been informed of my legal right of withdrawal, which allows me to cancel my registration within 14 days from the date of payment."
1365
+ },
1366
+ "components.SaleTunnel.WithdrawRightCheckbox.certificate.waiverLabel": {
1367
+ "description": "Text to explain why the user has to waive to its withdrawal right.",
1368
+ "message": "If you access the exam, you acknowledge waiving your 14-day withdrawal right, as provided for in Article L221-18 of the French Consumer Code."
1369
+ },
1370
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause1": {
1371
+ "description": "First clause item for the waiver checkbox.",
1372
+ "message": "I acknowledge that I have expressly requested to begin the training before the expiration date of the withdrawal period."
1373
+ },
1374
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause2": {
1375
+ "description": "Second clause item for the waiver checkbox.",
1376
+ "message": "I expressly waive my right of withdrawal in order to begin the training before the expiration of the withdrawal period."
1377
+ },
1378
+ "components.SaleTunnel.WithdrawRightCheckbox.credential.waiverLabel": {
1379
+ "description": "Text to explain why the user has to waive to its withdrawal right.",
1380
+ "message": "The training program you wish to enroll in begins before the end of the 14-day withdrawal period mentioned in Article L221-18 of the French Consumer Code. You must check the box below to proceed with your registration."
1381
+ },
1382
+ "components.SaleTunnel.WithdrawRightCheckbox.waiveCheckboxLabel": {
1383
+ "description": "Label of the checkbox to waive the withdrawal right.",
1384
+ "message": "By checking this box:"
1385
+ },
1358
1386
  "components.SaleTunnel.callToActionDescription": {
1359
1387
  "description": "Additional description announced by screen readers when focusing the call to action buying button",
1360
1388
  "message": "Purchase {product}"
@@ -1535,6 +1563,10 @@
1535
1563
  "description": "Error message shown when order creation request failed because there is no remaining available seat for the product.",
1536
1564
  "message": "There are no more places available for this product."
1537
1565
  },
1566
+ "components.SubscriptionButton.errorWithdrawalRight": {
1567
+ "description": "Error message shown when the user must waive its withdrawal right but doesn't.",
1568
+ "message": "You must waive your withdrawal right."
1569
+ },
1538
1570
  "components.SubscriptionButton.orderCreationInProgress": {
1539
1571
  "description": "Label for screen reader when an order creation is in progress.",
1540
1572
  "message": "Order creation in progress"
@@ -22,7 +22,7 @@ describe('Fonzie API', () => {
22
22
 
23
23
  it('uses its own route to get user information', async () => {
24
24
  const user = {
25
- username: faker.internet.userName(),
25
+ username: faker.internet.username(),
26
26
  };
27
27
 
28
28
  fetchMock.get('https://demo.endpoint.api/api/v1.0/user/me', user);
@@ -48,7 +48,7 @@ describe('OpenEdX Hawthorn API', () => {
48
48
  describe('enrollment', () => {
49
49
  beforeEach(() => {
50
50
  courseId = faker.string.uuid();
51
- username = faker.internet.userName();
51
+ username = faker.internet.username();
52
52
  fetchMock.restore();
53
53
  mockHandle.mockRestore();
54
54
  });
@@ -3,14 +3,14 @@ import { generatePath } from 'react-router-dom';
3
3
  import { Course as RichieCourse, isRichieCourse } from 'types/Course';
4
4
  import {
5
5
  CourseListItem as JoanieCourse,
6
- CourseProductRelation,
6
+ CourseProductRelationLight,
7
7
  isCourseProductRelation,
8
8
  } from 'types/Joanie';
9
9
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
10
10
  import { CourseGlimpseCourse } from '.';
11
11
 
12
12
  const getCourseGlimpsePropsFromCourseProductRelation = (
13
- courseProductRelation: CourseProductRelation,
13
+ courseProductRelation: CourseProductRelationLight,
14
14
  intl: IntlShape,
15
15
  organizationId?: string,
16
16
  ): CourseGlimpseCourse => {
@@ -95,7 +95,7 @@ const getCourseGlimpsePropsFromJoanieCourse = (
95
95
  };
96
96
 
97
97
  export const getCourseGlimpseProps = (
98
- course: RichieCourse | (JoanieCourse | CourseProductRelation),
98
+ course: RichieCourse | (JoanieCourse | CourseProductRelationLight),
99
99
  intl?: IntlShape,
100
100
  organizationId?: string,
101
101
  ): CourseGlimpseCourse => {
@@ -1,10 +1,10 @@
1
1
  import { IntlShape } from 'react-intl';
2
- import { CourseProductRelation, CourseListItem as JoanieCourse } from 'types/Joanie';
2
+ import { CourseProductRelationLight, CourseListItem as JoanieCourse } from 'types/Joanie';
3
3
  import { Course as RichieCourse } from 'types/Course';
4
4
  import { CourseGlimpseCourse, getCourseGlimpseProps } from 'components/CourseGlimpse';
5
5
 
6
6
  export const getCourseGlimpseListProps = (
7
- courses: RichieCourse[] | (JoanieCourse | CourseProductRelation)[],
7
+ courses: RichieCourse[] | (JoanieCourse | CourseProductRelationLight)[],
8
8
  intl?: IntlShape,
9
9
  organizationId?: string,
10
10
  ): CourseGlimpseCourse[] => {
@@ -8,6 +8,7 @@ export enum SubscriptionErrorMessageId {
8
8
  ERROR_ADDRESS = 'errorAddress',
9
9
  ERROR_DEFAULT = 'errorDefault',
10
10
  ERROR_FULL_PRODUCT = 'errorFullProduct',
11
+ ERROR_WITHDRAWAL_RIGHT = 'errorWithdrawalRight',
11
12
  }
12
13
 
13
14
  export enum PaymentProviders {
@@ -91,6 +91,7 @@ describe('PurchaseButton', () => {
91
91
  product={product}
92
92
  disabled={false}
93
93
  course={PacedCourseFactory({ code: '00000' }).one()}
94
+ isWithdrawable={true}
94
95
  />
95
96
  </Wrapper>,
96
97
  );
@@ -122,6 +123,7 @@ describe('PurchaseButton', () => {
122
123
  product={product}
123
124
  disabled={false}
124
125
  course={PacedCourseFactory({ code: courseCode }).one()}
126
+ isWithdrawable={true}
125
127
  />
126
128
  </Wrapper>,
127
129
  );
@@ -164,6 +166,7 @@ describe('PurchaseButton', () => {
164
166
  product={product}
165
167
  disabled={false}
166
168
  course={PacedCourseFactory({ code: courseCode }).one()}
169
+ isWithdrawable={true}
167
170
  />
168
171
  </Wrapper>,
169
172
  );
@@ -207,6 +210,7 @@ describe('PurchaseButton', () => {
207
210
  product={product}
208
211
  disabled={false}
209
212
  course={PacedCourseFactory({ code: courseCode }).one()}
213
+ isWithdrawable={true}
210
214
  />
211
215
  </Wrapper>,
212
216
  );
@@ -243,6 +247,7 @@ describe('PurchaseButton', () => {
243
247
  product={product}
244
248
  disabled={false}
245
249
  course={PacedCourseFactory({ code: courseCode }).one()}
250
+ isWithdrawable={true}
246
251
  />
247
252
  </Wrapper>,
248
253
  );
@@ -284,6 +289,7 @@ describe('PurchaseButton', () => {
284
289
  product={product}
285
290
  disabled={false}
286
291
  course={PacedCourseFactory({ code: courseCode }).one()}
292
+ isWithdrawable={true}
287
293
  />
288
294
  </Wrapper>,
289
295
  );
@@ -333,7 +339,12 @@ describe('PurchaseButton', () => {
333
339
 
334
340
  render(
335
341
  <Wrapper client={createTestQueryClient({ user: true })}>
336
- <PurchaseButton product={product} disabled={false} enrollment={enrollment} />
342
+ <PurchaseButton
343
+ product={product}
344
+ disabled={false}
345
+ enrollment={enrollment}
346
+ isWithdrawable={true}
347
+ />
337
348
  </Wrapper>,
338
349
  );
339
350
 
@@ -390,7 +401,12 @@ describe('PurchaseButton', () => {
390
401
 
391
402
  render(
392
403
  <Wrapper client={createTestQueryClient({ user: true })}>
393
- <PurchaseButton product={product} disabled={false} enrollment={enrollment} />
404
+ <PurchaseButton
405
+ product={product}
406
+ disabled={false}
407
+ enrollment={enrollment}
408
+ isWithdrawable={true}
409
+ />
394
410
  </Wrapper>,
395
411
  );
396
412
 
@@ -428,6 +444,7 @@ describe('PurchaseButton', () => {
428
444
  product={product}
429
445
  disabled={false}
430
446
  course={PacedCourseFactory({ code: courseCode }).one()}
447
+ isWithdrawable={true}
431
448
  />
432
449
  </Wrapper>,
433
450
  );
@@ -462,6 +479,7 @@ describe('PurchaseButton', () => {
462
479
  product={product}
463
480
  disabled={true}
464
481
  course={PacedCourseFactory({ code: courseCode }).one()}
482
+ isWithdrawable={true}
465
483
  />
466
484
  </Wrapper>,
467
485
  );
@@ -43,6 +43,7 @@ const messages = defineMessages({
43
43
  interface PurchaseButtonPropsBase {
44
44
  product: Joanie.CredentialProduct | Joanie.CertificateProduct;
45
45
  orderGroup?: Joanie.OrderGroup;
46
+ isWithdrawable: boolean;
46
47
  disabled?: boolean;
47
48
  className?: string;
48
49
  buttonProps?: ButtonProps;
@@ -67,6 +68,7 @@ const PurchaseButton = ({
67
68
  course,
68
69
  enrollment,
69
70
  orderGroup,
71
+ isWithdrawable,
70
72
  organizations,
71
73
  disabled = false,
72
74
  className,
@@ -141,6 +143,7 @@ const PurchaseButton = ({
141
143
  enrollment={enrollment}
142
144
  orderGroup={orderGroup}
143
145
  course={course}
146
+ isWithdrawable={isWithdrawable}
144
147
  onFinish={onFinish}
145
148
  />
146
149
  </>
@@ -63,6 +63,8 @@ describe('AddressSelector', () => {
63
63
  unregisterSubmitCallback: jest.fn(),
64
64
  runSubmitCallbacks: jest.fn(),
65
65
  nextStep: jest.fn(),
66
+ hasWaivedWithdrawalRight: false,
67
+ setHasWaivedWithdrawalRight: jest.fn(),
66
68
  }),
67
69
  [billingAddress],
68
70
  );
@@ -35,6 +35,8 @@ export interface SaleTunnelContextType {
35
35
  setBillingAddress: (address?: Address) => void;
36
36
  creditCard?: CreditCard;
37
37
  setCreditCard: (creditCard?: CreditCard) => void;
38
+ hasWaivedWithdrawalRight: boolean;
39
+ setHasWaivedWithdrawalRight: (hasWaivedWithdrawalRight: boolean) => void;
38
40
  registerSubmitCallback: (key: string, callback: () => Promise<void>) => void;
39
41
  unregisterSubmitCallback: (key: string) => void;
40
42
  runSubmitCallbacks: () => Promise<void>;
@@ -76,6 +78,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
76
78
  });
77
79
  const [billingAddress, setBillingAddress] = useState<Address>();
78
80
  const [creditCard, setCreditCard] = useState<CreditCard>();
81
+ const [hasWaivedWithdrawalRight, setHasWaivedWithdrawalRight] = useState(false);
79
82
  const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
80
83
  const [submitCallbacks, setSubmitCallbacks] = useState<Map<string, () => Promise<void>>>(
81
84
  new Map(),
@@ -115,6 +118,8 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
115
118
  setBillingAddress,
116
119
  creditCard,
117
120
  setCreditCard,
121
+ hasWaivedWithdrawalRight,
122
+ setHasWaivedWithdrawalRight,
118
123
  nextStep,
119
124
  step,
120
125
  registerSubmitCallback: (key, callback) => {
@@ -131,7 +136,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
131
136
  await Promise.all(Array.from(submitCallbacks.values()).map((cb) => cb()));
132
137
  },
133
138
  }),
134
- [props, order, billingAddress, creditCard, step, submitCallbacks],
139
+ [props, order, billingAddress, creditCard, step, submitCallbacks, hasWaivedWithdrawalRight],
135
140
  );
136
141
 
137
142
  return (
@@ -7,6 +7,7 @@ import { useSession } from 'contexts/SessionContext';
7
7
  import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
8
8
  import { usePaymentSchedule } from 'hooks/usePaymentSchedule';
9
9
  import { Spinner } from 'components/Spinner';
10
+ import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
10
11
 
11
12
  const messages = defineMessages({
12
13
  title: {
@@ -71,6 +72,7 @@ export const SaleTunnelInformation = () => {
71
72
  <div>
72
73
  <PaymentScheduleBlock />
73
74
  <Total />
75
+ <WithdrawRightCheckbox />
74
76
  </div>
75
77
  </div>
76
78
  );
@@ -4,4 +4,12 @@
4
4
  margin-top: 0.5rem;
5
5
  margin-bottom: 0;
6
6
  }
7
+
8
+ &__waiveCheckbox {
9
+ & > .waiveCheckbox__input {
10
+ /* Just add 1 px offset to prevent border input to be hidden
11
+ due to the overflow hidden applied to the parent block */
12
+ margin-left: 1px;
13
+ }
14
+ }
7
15
  }
@@ -1,4 +1,4 @@
1
- import { useMemo, useEffect, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
  import { Alert, Button, VariantType } from '@openfun/cunningham-react';
3
3
  import { defineMessages, FormattedMessage } from 'react-intl';
4
4
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
@@ -50,6 +50,11 @@ const messages = defineMessages({
50
50
  description: "Error message shown when the user didn't select a billing address.",
51
51
  id: 'components.SubscriptionButton.errorAddress',
52
52
  },
53
+ errorWithdrawalRight: {
54
+ defaultMessage: 'You must waive your withdrawal right.',
55
+ description: "Error message shown when the user must waive its withdrawal right but doesn't.",
56
+ id: 'components.SubscriptionButton.errorWithdrawalRight',
57
+ },
53
58
  orderCreationInProgress: {
54
59
  defaultMessage: 'Order creation in progress',
55
60
  description: 'Label for screen reader when an order creation is in progress.',
@@ -65,7 +70,10 @@ enum ComponentStates {
65
70
 
66
71
  interface Props {
67
72
  buildOrderPayload: (
68
- payload: Pick<OrderCreationPayload, 'product_id' | 'billing_address' | 'order_group_id'>,
73
+ payload: Pick<
74
+ OrderCreationPayload,
75
+ 'product_id' | 'billing_address' | 'order_group_id' | 'has_waived_withdrawal_right'
76
+ >,
69
77
  ) => OrderCreationPayload;
70
78
  }
71
79
 
@@ -74,6 +82,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
74
82
  order,
75
83
  creditCard,
76
84
  billingAddress,
85
+ hasWaivedWithdrawalRight,
77
86
  product,
78
87
  nextStep,
79
88
  runSubmitCallbacks,
@@ -107,10 +116,16 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
107
116
  return;
108
117
  }
109
118
 
119
+ if (!saleTunnelProps.isWithdrawable && !hasWaivedWithdrawalRight) {
120
+ handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
121
+ return;
122
+ }
123
+
110
124
  const payload = buildOrderPayload({
111
125
  product_id: product.id,
112
126
  billing_address: billingAddress!,
113
127
  order_group_id: saleTunnelProps.orderGroup?.id,
128
+ has_waived_withdrawal_right: hasWaivedWithdrawalRight,
114
129
  });
115
130
 
116
131
  orderMethods.create(payload, {
@@ -0,0 +1,105 @@
1
+ import { Alert, Checkbox, VariantType } from '@openfun/cunningham-react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
4
+ import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
5
+ import { ProductType } from 'types/Joanie';
6
+
7
+ const messages = defineMessages({
8
+ waiveCheckboxLabel: {
9
+ defaultMessage: 'By checking this box:',
10
+ description: 'Label of the checkbox to waive the withdrawal right.',
11
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.waiveCheckboxLabel',
12
+ },
13
+ });
14
+
15
+ const credentialProductMessages = defineMessages({
16
+ waiveCheckboxExplanation: {
17
+ defaultMessage:
18
+ 'The training program you wish to enroll in begins before the end of the 14-day withdrawal period mentioned in Article L221-18 of the French Consumer Code. You must check the box below to proceed with your registration.',
19
+ description: 'Text to explain why the user has to waive to its withdrawal right.',
20
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.credential.waiverLabel',
21
+ },
22
+ waiveCheckboxHelperClause1: {
23
+ defaultMessage:
24
+ 'I acknowledge that I have expressly requested to begin the training before the expiration date of the withdrawal period.',
25
+ description: 'First clause item for the waiver checkbox.',
26
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause1',
27
+ },
28
+ waiveCheckboxHelperClause2: {
29
+ defaultMessage:
30
+ 'I expressly waive my right of withdrawal in order to begin the training before the expiration of the withdrawal period.',
31
+ description: 'Second clause item for the waiver checkbox.',
32
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.credential.waiveCheckboxHelperClause2',
33
+ },
34
+ });
35
+
36
+ const certificateProductMessages = defineMessages({
37
+ waiveCheckboxExplanation: {
38
+ defaultMessage:
39
+ 'If you access the exam, you acknowledge waiving your 14-day withdrawal right, as provided for in Article L221-18 of the French Consumer Code.',
40
+ description: 'Text to explain why the user has to waive to its withdrawal right.',
41
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.certificate.waiverLabel',
42
+ },
43
+ waiveCheckboxHelperClause1: {
44
+ defaultMessage:
45
+ 'I acknowledge that I have been informed of my legal right of withdrawal, which allows me to cancel my registration within 14 days from the date of payment.',
46
+ description: 'First clause item for the waiver checkbox.',
47
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.certificate.waiveCheckboxHelperClause1',
48
+ },
49
+ waiveCheckboxHelperClause2: {
50
+ defaultMessage:
51
+ 'I understand that if I access the exam during this period, I expressly waive my right of withdrawal.',
52
+ description: 'Second clause item for the waiver checkbox.',
53
+ id: 'components.SaleTunnel.WithdrawRightCheckbox.certificate .waiveCheckboxHelperClause2',
54
+ },
55
+ });
56
+
57
+ const WithdrawRightCheckbox = () => {
58
+ const {
59
+ props: { isWithdrawable, product },
60
+ registerSubmitCallback,
61
+ unregisterSubmitCallback,
62
+ hasWaivedWithdrawalRight,
63
+ setHasWaivedWithdrawalRight,
64
+ } = useSaleTunnelContext();
65
+ const intl = useIntl();
66
+ const clauseMessages =
67
+ product.type === ProductType.CERTIFICATE
68
+ ? certificateProductMessages
69
+ : credentialProductMessages;
70
+ const [hasErrorState, setHasError] = useState(false);
71
+ const setError = useCallback(async () => {
72
+ setHasError(!isWithdrawable && !hasWaivedWithdrawalRight);
73
+ }, [hasWaivedWithdrawalRight, isWithdrawable]);
74
+
75
+ useEffect(() => {
76
+ registerSubmitCallback('withdrawalRight', setError);
77
+ return () => {
78
+ unregisterSubmitCallback('withdrawalRight');
79
+ };
80
+ }, [setError]);
81
+
82
+ if (isWithdrawable) return null;
83
+ return (
84
+ <section
85
+ className="mt-t subscription-button__waiveCheckbox"
86
+ data-testid="withdraw-right-checkbox"
87
+ >
88
+ <Alert type={hasErrorState ? VariantType.ERROR : VariantType.WARNING} className="mb-s">
89
+ <FormattedMessage {...clauseMessages.waiveCheckboxExplanation} />
90
+ </Alert>
91
+ <Checkbox
92
+ className="waiveCheckbox__input"
93
+ label={intl.formatMessage(messages.waiveCheckboxLabel)}
94
+ checked={hasWaivedWithdrawalRight}
95
+ onChange={(e) => setHasWaivedWithdrawalRight(e.target.checked)}
96
+ textItems={[
97
+ intl.formatMessage(clauseMessages.waiveCheckboxHelperClause1),
98
+ intl.formatMessage(clauseMessages.waiveCheckboxHelperClause2),
99
+ ]}
100
+ />
101
+ </section>
102
+ );
103
+ };
104
+
105
+ export default WithdrawRightCheckbox;
@@ -53,8 +53,8 @@ describe('SaleTunnel / Credential', () => {
53
53
  let richieUser: User;
54
54
  let openApiEdxProfile: OpenEdxApiProfile;
55
55
 
56
- const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
57
- return <SaleTunnel {...props} isOpen={true} onClose={() => {}} />;
56
+ const Wrapper = (props: Omit<SaleTunnelProps, 'isWithdrawable' | 'isOpen' | 'onClose'>) => {
57
+ return <SaleTunnel {...props} isWithdrawable={true} isOpen={true} onClose={() => {}} />;
58
58
  };
59
59
 
60
60
  setupJoanieSession();
@@ -20,6 +20,7 @@ import {
20
20
  CreditCardFactory,
21
21
  PaymentFactory,
22
22
  PaymentInstallmentFactory,
23
+ ProductFactory,
23
24
  } from 'utils/test/factories/joanie';
24
25
  import { CourseRun, NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
25
26
  import { Priority } from 'types';
@@ -97,9 +98,13 @@ describe('SaleTunnel', () => {
97
98
  * Initialization.
98
99
  */
99
100
  const course = PacedCourseFactory().one();
100
- const relation = CourseProductRelationFactory({ course }).one();
101
+ const product = ProductFactory().one();
102
+ const relation = CourseProductRelationFactory({
103
+ course,
104
+ product,
105
+ is_withdrawable: false,
106
+ }).one();
101
107
  const paymentSchedule = PaymentInstallmentFactory().many(2);
102
- const { product } = relation;
103
108
 
104
109
  fetchMock.get(
105
110
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
@@ -265,6 +270,13 @@ describe('SaleTunnel', () => {
265
270
  priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
266
271
  );
267
272
 
273
+ /**
274
+ * Make sure the checkbox to waive withdrawal right is displayed
275
+ */
276
+ const $waiveCheckbox = within(screen.getByTestId('withdraw-right-checkbox')).getByRole(
277
+ 'checkbox',
278
+ );
279
+
268
280
  /**
269
281
  * Subscribe
270
282
  */
@@ -282,6 +294,14 @@ describe('SaleTunnel', () => {
282
294
  }) as HTMLButtonElement;
283
295
  await user.click($button);
284
296
 
297
+ /**
298
+ * An error should be displayed if the user has not waived its withdrawal right.
299
+ */
300
+ screen.getByText('You must waive your withdrawal right.');
301
+
302
+ await user.click($waiveCheckbox);
303
+ await user.click($button);
304
+
285
305
  order.state = OrderState.TO_SAVE_PAYMENT_METHOD;
286
306
  order.contract = ContractFactory({ student_signed_on: new Date().toISOString() }).one();
287
307