richie-education 2.28.2-dev26 → 2.28.2-dev53

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 (89) hide show
  1. package/js/api/joanie.ts +42 -17
  2. package/js/api/lms/dummy.ts +1 -12
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  5. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  6. package/js/components/ContractFrame/_styles.scss +6 -14
  7. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
  8. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +17 -24
  9. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  10. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  11. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  12. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  13. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  14. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  15. package/js/components/PaymentInterfaces/types.ts +5 -2
  16. package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
  17. package/js/components/PaymentScheduleGrid/index.tsx +50 -70
  18. package/js/components/PurchaseButton/index.spec.tsx +84 -37
  19. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  20. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  21. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  22. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +80 -27
  23. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
  24. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  25. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  26. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  27. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
  28. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
  29. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  30. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  31. package/js/components/SaleTunnel/_styles.scss +16 -5
  32. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  33. package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
  34. package/js/components/SaleTunnel/index.full-process.spec.tsx +116 -48
  35. package/js/components/SaleTunnel/index.spec.tsx +334 -717
  36. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  37. package/js/components/SignContractButton/index.spec.tsx +16 -20
  38. package/js/components/SignContractButton/index.tsx +3 -1
  39. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  40. package/js/hooks/useCreditCards/index.ts +49 -11
  41. package/js/hooks/useOrders/index.spec.tsx +322 -0
  42. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  43. package/js/hooks/usePaymentSchedule.tsx +23 -0
  44. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  45. package/js/hooks/useProductOrder/index.tsx +2 -2
  46. package/js/hooks/useResources/useResourcesRoot.ts +4 -3
  47. package/js/index.tsx +2 -0
  48. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  49. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  50. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  51. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  53. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  54. package/js/settings/settings.test.ts +11 -2
  55. package/js/types/Joanie.ts +77 -31
  56. package/js/utils/OrderHelper/index.ts +47 -38
  57. package/js/utils/test/factories/joanie.ts +66 -68
  58. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  59. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  60. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +114 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +99 -12
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +126 -0
  67. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +209 -0
  68. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
  70. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
  71. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  72. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  73. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  74. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  77. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  78. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  79. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  83. package/package.json +2 -1
  84. package/scss/components/_index.scss +4 -2
  85. package/js/components/PaymentButton/_styles.scss +0 -27
  86. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -338
  87. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  88. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
  89. /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
@@ -4,6 +4,7 @@ import { IntlProvider } from 'react-intl';
4
4
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import { CunninghamProvider } from '@openfun/cunningham-react';
7
+ import queryString from 'query-string';
7
8
  import {
8
9
  CourseStateFactory,
9
10
  PacedCourseFactory,
@@ -17,7 +18,7 @@ import {
17
18
  } from 'utils/test/factories/joanie';
18
19
  import { SessionProvider } from 'contexts/SessionContext';
19
20
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
20
- import { ProductType } from 'types/Joanie';
21
+ import { NOT_CANCELED_ORDER_STATES, ProductType } from 'types/Joanie';
21
22
  import { Priority } from 'types';
22
23
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
23
24
  import { User } from 'types/User';
@@ -102,10 +103,18 @@ describe('PurchaseButton', () => {
102
103
  it('shows cta to open sale tunnel when user is authenticated', async () => {
103
104
  const courseCode = '00000';
104
105
  const product = ProductFactory().one();
105
- fetchMock.get(
106
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
107
- {},
108
- );
106
+ const orderQueryParameters = {
107
+ course_code: courseCode,
108
+ product_id: product.id,
109
+ state: NOT_CANCELED_ORDER_STATES,
110
+ };
111
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
112
+ fetchMock
113
+ .get(url, {})
114
+ .get(
115
+ `https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
116
+ [],
117
+ );
109
118
 
110
119
  render(
111
120
  <Wrapper client={createTestQueryClient({ user: richieUser })}>
@@ -137,10 +146,18 @@ describe('PurchaseButton', () => {
137
146
  const product = ProductFactory({ remaining_order_count: null }).one();
138
147
  fetchMock.get(`https://demo.endpoint/api/user/v1/accounts/${user.username}`, {});
139
148
  fetchMock.get(`https://demo.endpoint/api/user/v1/preferences/${user.username}`, {});
140
- fetchMock.get(
141
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
142
- {},
143
- );
149
+ const orderQueryParameters = {
150
+ course_code: courseCode,
151
+ product_id: product.id,
152
+ state: NOT_CANCELED_ORDER_STATES,
153
+ };
154
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
155
+ fetchMock
156
+ .get(url, {})
157
+ .get(
158
+ `https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
159
+ [],
160
+ );
144
161
  render(
145
162
  <Wrapper client={createTestQueryClient({ user })}>
146
163
  <PurchaseButton
@@ -170,10 +187,18 @@ describe('PurchaseButton', () => {
170
187
  it('shows cta to open sale tunnel when remaining orders is undefined', async () => {
171
188
  const courseCode = '00000';
172
189
  const product = ProductFactory().one();
173
- fetchMock.get(
174
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
175
- {},
176
- );
190
+ const orderQueryParameters = {
191
+ course_code: courseCode,
192
+ product_id: product.id,
193
+ state: NOT_CANCELED_ORDER_STATES,
194
+ };
195
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
196
+ fetchMock
197
+ .get(url, {})
198
+ .get(
199
+ `https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${product.id}/payment-schedule/`,
200
+ [],
201
+ );
177
202
  delete product.remaining_order_count;
178
203
 
179
204
  render(
@@ -205,10 +230,13 @@ describe('PurchaseButton', () => {
205
230
  it('renders a disabled CTA if the product have no remaining orders', async () => {
206
231
  const courseCode = '00000';
207
232
  const product = ProductFactory({ remaining_order_count: 0 }).one();
208
- fetchMock.get(
209
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
210
- {},
211
- );
233
+ const orderQueryParameters = {
234
+ course_code: courseCode,
235
+ product_id: product.id,
236
+ state: NOT_CANCELED_ORDER_STATES,
237
+ };
238
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
239
+ fetchMock.get(url, {});
212
240
  render(
213
241
  <Wrapper client={createTestQueryClient({ user: richieUser })}>
214
242
  <PurchaseButton
@@ -241,10 +269,14 @@ describe('PurchaseButton', () => {
241
269
  const courseCode = '00000';
242
270
  const product = ProductFactory({ ...productData, type: ProductType.CREDENTIAL }).one();
243
271
  product.target_courses[0].course_runs = [];
244
- fetchMock.get(
245
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
246
- {},
247
- );
272
+
273
+ const orderQueryParameters = {
274
+ course_code: courseCode,
275
+ product_id: product.id,
276
+ state: NOT_CANCELED_ORDER_STATES,
277
+ };
278
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
279
+ fetchMock.get(url, {});
248
280
 
249
281
  render(
250
282
  <Wrapper client={createTestQueryClient({ user: richieUser })}>
@@ -290,10 +322,14 @@ describe('PurchaseButton', () => {
290
322
  const product = CertificateProductFactory().one();
291
323
  const enrollment = EnrollmentFactory().one();
292
324
  enrollment.course_run.state = CourseStateFactory(courseRunStateData).one();
293
- fetchMock.get(
294
- `https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollment.id}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
295
- {},
296
- );
325
+
326
+ const orderQueryParameters = {
327
+ enrollment_id: enrollment.id,
328
+ product_id: product.id,
329
+ state: NOT_CANCELED_ORDER_STATES,
330
+ };
331
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
332
+ fetchMock.get(url, {});
297
333
 
298
334
  render(
299
335
  <Wrapper client={createTestQueryClient({ user: true })}>
@@ -344,10 +380,13 @@ describe('PurchaseButton', () => {
344
380
  const enrollment = EnrollmentFactory().one();
345
381
  enrollment.course_run.state = CourseStateFactory(courseRunStateData).one();
346
382
 
347
- fetchMock.get(
348
- `https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollment.id}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
349
- {},
350
- );
383
+ const orderQueryParameters = {
384
+ enrollment_id: enrollment.id,
385
+ product_id: product.id,
386
+ state: NOT_CANCELED_ORDER_STATES,
387
+ };
388
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
389
+ fetchMock.get(url, {});
351
390
 
352
391
  render(
353
392
  <Wrapper client={createTestQueryClient({ user: true })}>
@@ -373,10 +412,14 @@ describe('PurchaseButton', () => {
373
412
  it('renders a disabled CTA if product has no target courses', async () => {
374
413
  const courseCode = '00000';
375
414
  const product = ProductFactory().one();
376
- fetchMock.get(
377
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
378
- {},
379
- );
415
+
416
+ const orderQueryParameters = {
417
+ course_code: courseCode,
418
+ product_id: product.id,
419
+ state: NOT_CANCELED_ORDER_STATES,
420
+ };
421
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
422
+ fetchMock.get(url, {});
380
423
  product.target_courses = [];
381
424
 
382
425
  render(
@@ -404,10 +447,14 @@ describe('PurchaseButton', () => {
404
447
  it('does not render CTA if disabled property is false', async () => {
405
448
  const courseCode = '00000';
406
449
  const product = ProductFactory().one();
407
- fetchMock.get(
408
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${courseCode}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
409
- {},
410
- );
450
+
451
+ const orderQueryParameters = {
452
+ course_code: courseCode,
453
+ product_id: product.id,
454
+ state: NOT_CANCELED_ORDER_STATES,
455
+ };
456
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
457
+ fetchMock.get(url, {});
411
458
 
412
459
  render(
413
460
  <Wrapper client={createTestQueryClient({ user: true })}>
@@ -59,10 +59,11 @@ describe('AddressSelector', () => {
59
59
  setBillingAddress,
60
60
  setCreditCard: jest.fn(),
61
61
  onPaymentSuccess: jest.fn(),
62
- step: SaleTunnelStep.PAYMENT,
62
+ step: SaleTunnelStep.IDLE,
63
63
  registerSubmitCallback: jest.fn(),
64
64
  unregisterSubmitCallback: jest.fn(),
65
65
  runSubmitCallbacks: jest.fn(),
66
+ nextStep: jest.fn(),
66
67
  }),
67
68
  [billingAddress],
68
69
  );
@@ -1,8 +1,8 @@
1
1
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
2
2
  import { CertificateProduct } from 'types/Joanie';
3
3
  import { GenericSaleTunnel } from 'components/SaleTunnel/GenericSaleTunnel';
4
- import { GenericPaymentButton } from 'components/SaleTunnel/GenericPaymentButton';
5
4
  import { CertificateProductPath } from 'components/SaleTunnel/CertificateSaleTunnel/CertificateProductPath';
5
+ import SubscriptionButton from 'components/SaleTunnel/SubscriptionButton';
6
6
 
7
7
  interface CertificateSaleTunnelProps extends Omit<SaleTunnelProps, 'product'> {
8
8
  product: CertificateProduct;
@@ -23,7 +23,7 @@ const CertificatePaymentButton = ({
23
23
  enrollment,
24
24
  }: Pick<CertificateSaleTunnelProps, 'enrollment'>) => {
25
25
  return (
26
- <GenericPaymentButton
26
+ <SubscriptionButton
27
27
  buildOrderPayload={(payload) => {
28
28
  return {
29
29
  ...payload,
@@ -1,8 +1,8 @@
1
1
  import { CredentialProduct } from 'types/Joanie';
2
2
  import { GenericSaleTunnel } from 'components/SaleTunnel/GenericSaleTunnel';
3
3
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
4
- import { GenericPaymentButton } from 'components/SaleTunnel/GenericPaymentButton';
5
4
  import { CredentialProductPath } from 'components/SaleTunnel/CredentialSaleTunnel/CredentialProductPath';
5
+ import SubscriptionButton from 'components/SaleTunnel/SubscriptionButton';
6
6
 
7
7
  interface CredentialSaleTunnelProps extends Omit<SaleTunnelProps, 'product'> {
8
8
  product: CredentialProduct;
@@ -21,17 +21,13 @@ export const CredentialSaleTunnel = (props: CredentialSaleTunnelProps) => {
21
21
 
22
22
  const CredentialPaymentButton = ({
23
23
  course,
24
- orderGroup,
25
24
  }: Pick<CredentialSaleTunnelProps, 'course' | 'orderGroup'>) => {
26
25
  return (
27
- <GenericPaymentButton
28
- buildOrderPayload={(payload) => {
29
- return {
30
- ...payload,
31
- course_code: course!.code,
32
- ...(orderGroup ? { order_group_id: orderGroup.id } : {}),
33
- };
34
- }}
26
+ <SubscriptionButton
27
+ buildOrderPayload={(payload) => ({
28
+ ...payload,
29
+ course_code: course!.code,
30
+ })}
35
31
  />
36
32
  );
37
33
  };
@@ -1,8 +1,16 @@
1
1
  import { Modal, ModalSize } from '@openfun/cunningham-react';
2
- import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ createContext,
4
+ ReactNode,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ useCallback,
10
+ } from 'react';
3
11
  import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
4
12
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
5
- import { Address, CreditCard, Order, Product } from 'types/Joanie';
13
+ import { Address, CreditCard, Order, OrderState, Product } from 'types/Joanie';
6
14
  import useProductOrder from 'hooks/useProductOrder';
7
15
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
8
16
  import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -10,7 +18,8 @@ import { CourseProductEvent } from 'types/web-analytics';
10
18
  import { useOmniscientOrders, useOrders } from 'hooks/useOrders';
11
19
  import { SaleTunnelInformation } from 'components/SaleTunnel/SaleTunnelInformation';
12
20
  import { useEnrollments } from 'hooks/useEnrollments';
13
- import SaleTunnelNotValidated from './SaleTunnelNotValidated';
21
+ import SaleTunnelSavePaymentMethod from 'components/SaleTunnel/SaleTunnelSavePaymentMethod';
22
+ import { LearnerContractFrame } from 'components/ContractFrame';
14
23
 
15
24
  export interface SaleTunnelContextType {
16
25
  props: SaleTunnelProps;
@@ -19,7 +28,7 @@ export interface SaleTunnelContextType {
19
28
  webAnalyticsEventKey: string;
20
29
 
21
30
  // internal
22
- onPaymentSuccess: (validated?: boolean) => void;
31
+ onPaymentSuccess: () => void;
23
32
  step: SaleTunnelStep;
24
33
 
25
34
  // meta
@@ -30,6 +39,7 @@ export interface SaleTunnelContextType {
30
39
  registerSubmitCallback: (key: string, callback: () => Promise<void>) => void;
31
40
  unregisterSubmitCallback: (key: string) => void;
32
41
  runSubmitCallbacks: () => Promise<void>;
42
+ nextStep: () => void;
33
43
  }
34
44
 
35
45
  export const SaleTunnelContext = createContext<SaleTunnelContextType>({} as any);
@@ -45,9 +55,10 @@ export const useSaleTunnelContext = () => {
45
55
  };
46
56
 
47
57
  export enum SaleTunnelStep {
48
- PAYMENT,
58
+ IDLE,
59
+ SIGN,
60
+ SAVE_PAYMENT,
49
61
  SUCCESS,
50
- NOT_VALIDATED,
51
62
  }
52
63
 
53
64
  interface GenericSaleTunnelProps extends SaleTunnelProps {
@@ -76,11 +87,35 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
76
87
  } = useEnrollments(undefined, { enabled: false });
77
88
  const [billingAddress, setBillingAddress] = useState<Address>();
78
89
  const [creditCard, setCreditCard] = useState<CreditCard>();
79
- const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.PAYMENT);
90
+ const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
80
91
  const [submitCallbacks, setSubmitCallbacks] = useState<Map<string, () => Promise<void>>>(
81
92
  new Map(),
82
93
  );
83
94
 
95
+ const nextStep = useCallback(() => {
96
+ if (order)
97
+ switch (step) {
98
+ case SaleTunnelStep.IDLE:
99
+ if ([OrderState.TO_SIGN, OrderState.SIGNING].includes(order.state)) {
100
+ setStep(SaleTunnelStep.SIGN);
101
+ } else if (order.state === OrderState.TO_SAVE_PAYMENT_METHOD) {
102
+ setStep(SaleTunnelStep.SAVE_PAYMENT);
103
+ }
104
+ break;
105
+ case SaleTunnelStep.SIGN:
106
+ if (order.state === OrderState.TO_SAVE_PAYMENT_METHOD) {
107
+ setStep(SaleTunnelStep.SAVE_PAYMENT);
108
+ }
109
+ if (order.state === OrderState.COMPLETED) {
110
+ setStep(SaleTunnelStep.SUCCESS);
111
+ }
112
+ break;
113
+ case SaleTunnelStep.SAVE_PAYMENT:
114
+ setStep(SaleTunnelStep.SUCCESS);
115
+ break;
116
+ }
117
+ }, [order, step]);
118
+
84
119
  const context: SaleTunnelContextType = useMemo(
85
120
  () => ({
86
121
  webAnalyticsEventKey: props.eventKey,
@@ -91,12 +126,9 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
91
126
  setBillingAddress,
92
127
  creditCard,
93
128
  setCreditCard,
94
- onPaymentSuccess: (validated: boolean = true) => {
95
- if (validated) {
96
- setStep(SaleTunnelStep.SUCCESS);
97
- } else {
98
- setStep(SaleTunnelStep.NOT_VALIDATED);
99
- }
129
+ nextStep,
130
+ onPaymentSuccess: () => {
131
+ nextStep();
100
132
  WebAnalyticsAPIHandler()?.sendCourseProductEvent(
101
133
  CourseProductEvent.PAYMENT_SUCCEED,
102
134
  props.eventKey,
@@ -144,13 +176,16 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
144
176
 
145
177
  export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
146
178
  const { step } = useSaleTunnelContext();
179
+
147
180
  switch (step) {
148
- case SaleTunnelStep.PAYMENT:
149
- return <GenericSaleTunnelPaymentStep {...props} />;
181
+ case SaleTunnelStep.IDLE:
182
+ return <GenericSaleTunnelInitialStep {...props} />;
183
+ case SaleTunnelStep.SIGN:
184
+ return <GenericSaleTunnelSignStep {...props} />;
185
+ case SaleTunnelStep.SAVE_PAYMENT:
186
+ return <GenericSaleTunnelSavePaymentMethodStep {...props} />;
150
187
  case SaleTunnelStep.SUCCESS:
151
188
  return <GenericSaleTunnelSuccessStep {...props} />;
152
- case SaleTunnelStep.NOT_VALIDATED:
153
- return <GenericSaleTunnelNotValidatedStep {...props} />;
154
189
  }
155
190
  throw new Error('Invalid step: ' + step);
156
191
  };
@@ -159,7 +194,7 @@ export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
159
194
  * Steps.
160
195
  */
161
196
 
162
- export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
197
+ export const GenericSaleTunnelInitialStep = (props: GenericSaleTunnelProps) => {
163
198
  const { webAnalyticsEventKey } = useSaleTunnelContext();
164
199
 
165
200
  useEffect(() => {
@@ -174,19 +209,21 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
174
209
  }, []);
175
210
 
176
211
  return (
177
- <Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title} closeOnEsc={false}>
212
+ <Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title}>
178
213
  <div className="sale-tunnel" data-testid="generic-sale-tunnel-payment-step">
179
214
  <div className="sale-tunnel__main">
180
- <div className="sale-tunnel__main__left">{props.asideNode}</div>
215
+ <div className="sale-tunnel__main__column sale-tunnel__main__left ">
216
+ <div>{props.asideNode}</div>
217
+ <div>
218
+ <SaleTunnelSponsors />
219
+ </div>
220
+ </div>
181
221
  <div className="sale-tunnel__main__separator" />
182
222
  <div className="sale-tunnel__main__right">
183
223
  <SaleTunnelInformation />
184
224
  </div>
185
225
  </div>
186
- <div className="sale-tunnel__footer">
187
- {props.paymentNode}
188
- <SaleTunnelSponsors />
189
- </div>
226
+ <div className="sale-tunnel__footer">{props.paymentNode}</div>
190
227
  </div>
191
228
  </Modal>
192
229
  );
@@ -200,10 +237,26 @@ export const GenericSaleTunnelSuccessStep = (props: SaleTunnelProps) => {
200
237
  );
201
238
  };
202
239
 
203
- export const GenericSaleTunnelNotValidatedStep = (props: SaleTunnelProps) => {
240
+ export const GenericSaleTunnelSavePaymentMethodStep = (props: SaleTunnelProps) => {
204
241
  return (
205
- <Modal {...props} size={ModalSize.MEDIUM}>
206
- <SaleTunnelNotValidated closeModal={props.onClose} />
242
+ <Modal {...props} size={ModalSize.LARGE}>
243
+ <SaleTunnelSavePaymentMethod />
207
244
  </Modal>
208
245
  );
209
246
  };
247
+
248
+ export const GenericSaleTunnelSignStep = ({ isOpen, onClose }: SaleTunnelProps) => {
249
+ const { order, nextStep } = useSaleTunnelContext();
250
+
251
+ const handleDone = useCallback(nextStep, [order]);
252
+
253
+ useEffect(() => {
254
+ if (![OrderState.TO_SIGN, OrderState.SIGNING].includes(order!.state)) {
255
+ nextStep();
256
+ }
257
+ }, [order]);
258
+
259
+ return (
260
+ <LearnerContractFrame order={order!} isOpen={isOpen} onClose={onClose} onDone={handleDone} />
261
+ );
262
+ };
@@ -1,12 +1,12 @@
1
- import { Alert, VariantType } from '@openfun/cunningham-react';
2
1
  import { defineMessages, FormattedMessage, FormattedNumber } from 'react-intl';
3
2
  import { AddressSelector } from 'components/SaleTunnel/AddressSelector';
4
- import { CreditCardSelector } from 'components/SaleTunnel/CreditCardSelector';
5
3
  import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
6
4
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
7
5
  import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm';
8
6
  import { useSession } from 'contexts/SessionContext';
9
7
  import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
8
+ import { usePaymentSchedule } from 'hooks/usePaymentSchedule';
9
+ import { Spinner } from 'components/Spinner';
10
10
 
11
11
  const messages = defineMessages({
12
12
  title: {
@@ -49,7 +49,7 @@ const messages = defineMessages({
49
49
 
50
50
  export const SaleTunnelInformation = () => {
51
51
  return (
52
- <div className="sale-tunnel__information">
52
+ <div className="sale-tunnel__main__column sale-tunnel__information">
53
53
  <div>
54
54
  <h3 className="block-title mb-t">
55
55
  <FormattedMessage {...messages.title} />
@@ -64,15 +64,12 @@ export const SaleTunnelInformation = () => {
64
64
  </div>
65
65
  </div>
66
66
  <div>
67
- <CreditCardSelector />
68
- </div>
69
- <div>
67
+ <PaymentScheduleBlock />
70
68
  <Total />
71
69
  </div>
72
70
  </div>
73
71
  );
74
72
  };
75
-
76
73
  const Email = () => {
77
74
  const { user } = useSession();
78
75
  const { data: openEdxProfileData } = useOpenEdxProfile({
@@ -98,9 +95,6 @@ const Total = () => {
98
95
  const { product } = useSaleTunnelContext();
99
96
  return (
100
97
  <div className="sale-tunnel__total">
101
- <Alert type={VariantType.INFO}>
102
- <FormattedMessage {...messages.totalInfo} />
103
- </Alert>
104
98
  <div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
105
99
  <div className="block-title">
106
100
  <FormattedMessage {...messages.totalLabel} />
@@ -117,20 +111,22 @@ const Total = () => {
117
111
  );
118
112
  };
119
113
 
120
- /**
121
- * Ready for V2.
122
- */
123
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
124
114
  const PaymentScheduleBlock = () => {
125
- return null;
115
+ const { props } = useSaleTunnelContext();
116
+ const query = usePaymentSchedule({
117
+ course_code: props.course?.code || props.enrollment!.course_run.course.code,
118
+ product_id: props.product.id,
119
+ });
120
+
121
+ if (!query.data || query.isLoading) {
122
+ return <Spinner size="large" />;
123
+ }
124
+
126
125
  return (
127
126
  <div className="payment-schedule">
128
- <h4 className="block-title mb-t">Schedule</h4>
129
- <Alert type={VariantType.INFO}>
130
- The first payment occurs in 14 days, you will be notified to pay the first 30%.
131
- </Alert>
127
+ <h4 className="block-title mb-t">Payment schedule</h4>
132
128
  <div className="mt-t">
133
- <PaymentScheduleGrid />
129
+ <PaymentScheduleGrid schedule={query.data} />
134
130
  </div>
135
131
  </div>
136
132
  );
@@ -0,0 +1,12 @@
1
+ .sale-tunnel-step--save-payment-method {
2
+ .credit-card-selector {
3
+ display: inline-block;
4
+ }
5
+
6
+ .sale-tunnel-step__footer {
7
+ align-items: center;
8
+ justify-content: center;
9
+ display: flex;
10
+ flex-direction: column;
11
+ }
12
+ }