richie-education 2.28.2-dev39 → 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 (82) hide show
  1. package/js/api/joanie.ts +12 -16
  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/CreditCardSelector/index.spec.tsx +7 -7
  8. package/js/components/CreditCardSelector/index.tsx +2 -2
  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/PurchaseButton/index.spec.tsx +69 -37
  17. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  18. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  19. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  20. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  21. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  22. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  23. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  24. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  25. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  26. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  27. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  28. package/js/components/SaleTunnel/_styles.scss +10 -1
  29. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  30. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  31. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  32. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  33. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  34. package/js/components/SignContractButton/index.spec.tsx +16 -20
  35. package/js/components/SignContractButton/index.tsx +3 -1
  36. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  37. package/js/hooks/useCreditCards/index.ts +49 -11
  38. package/js/hooks/useOrders/index.spec.tsx +322 -0
  39. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  40. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  41. package/js/hooks/useProductOrder/index.tsx +2 -2
  42. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  43. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  44. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  45. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  46. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  47. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  48. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  49. package/js/settings/settings.test.ts +11 -2
  50. package/js/types/Joanie.ts +49 -34
  51. package/js/utils/OrderHelper/index.ts +38 -42
  52. package/js/utils/test/factories/joanie.ts +36 -51
  53. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  54. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  55. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  59. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  60. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  67. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  68. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  71. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  72. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  73. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  74. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  77. package/package.json +1 -1
  78. package/scss/components/_index.scss +2 -1
  79. package/js/components/PaymentButton/_styles.scss +0 -27
  80. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  81. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
@@ -1,9 +1,16 @@
1
- import { Alert, Modal, ModalSize, VariantType } from '@openfun/cunningham-react';
2
- import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
3
- import { FormattedMessage, defineMessages } from 'react-intl';
1
+ import { Modal, ModalSize } from '@openfun/cunningham-react';
2
+ import {
3
+ createContext,
4
+ ReactNode,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ useCallback,
10
+ } from 'react';
4
11
  import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
5
12
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
6
- import { Address, CreditCard, Order, Product } from 'types/Joanie';
13
+ import { Address, CreditCard, Order, OrderState, Product } from 'types/Joanie';
7
14
  import useProductOrder from 'hooks/useProductOrder';
8
15
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
9
16
  import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -11,16 +18,8 @@ import { CourseProductEvent } from 'types/web-analytics';
11
18
  import { useOmniscientOrders, useOrders } from 'hooks/useOrders';
12
19
  import { SaleTunnelInformation } from 'components/SaleTunnel/SaleTunnelInformation';
13
20
  import { useEnrollments } from 'hooks/useEnrollments';
14
- import SaleTunnelNotValidated from './SaleTunnelNotValidated';
15
-
16
- const messages = defineMessages({
17
- walkthrough: {
18
- id: 'components.SaleTunnel.GenericSaleTunnel.walkthrough',
19
- defaultMessage:
20
- 'To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method. You will not be charged during this step; the first payment will take place on the date mentioned in the payment schedule above.',
21
- description: 'Message explaining the credential sale tunnel process',
22
- },
23
- });
21
+ import SaleTunnelSavePaymentMethod from 'components/SaleTunnel/SaleTunnelSavePaymentMethod';
22
+ import { LearnerContractFrame } from 'components/ContractFrame';
24
23
 
25
24
  export interface SaleTunnelContextType {
26
25
  props: SaleTunnelProps;
@@ -29,7 +28,7 @@ export interface SaleTunnelContextType {
29
28
  webAnalyticsEventKey: string;
30
29
 
31
30
  // internal
32
- onPaymentSuccess: (validated?: boolean) => void;
31
+ onPaymentSuccess: () => void;
33
32
  step: SaleTunnelStep;
34
33
 
35
34
  // meta
@@ -40,6 +39,7 @@ export interface SaleTunnelContextType {
40
39
  registerSubmitCallback: (key: string, callback: () => Promise<void>) => void;
41
40
  unregisterSubmitCallback: (key: string) => void;
42
41
  runSubmitCallbacks: () => Promise<void>;
42
+ nextStep: () => void;
43
43
  }
44
44
 
45
45
  export const SaleTunnelContext = createContext<SaleTunnelContextType>({} as any);
@@ -55,9 +55,10 @@ export const useSaleTunnelContext = () => {
55
55
  };
56
56
 
57
57
  export enum SaleTunnelStep {
58
- PAYMENT,
58
+ IDLE,
59
+ SIGN,
60
+ SAVE_PAYMENT,
59
61
  SUCCESS,
60
- NOT_VALIDATED,
61
62
  }
62
63
 
63
64
  interface GenericSaleTunnelProps extends SaleTunnelProps {
@@ -86,11 +87,35 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
86
87
  } = useEnrollments(undefined, { enabled: false });
87
88
  const [billingAddress, setBillingAddress] = useState<Address>();
88
89
  const [creditCard, setCreditCard] = useState<CreditCard>();
89
- const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.PAYMENT);
90
+ const [step, setStep] = useState<SaleTunnelStep>(SaleTunnelStep.IDLE);
90
91
  const [submitCallbacks, setSubmitCallbacks] = useState<Map<string, () => Promise<void>>>(
91
92
  new Map(),
92
93
  );
93
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
+
94
119
  const context: SaleTunnelContextType = useMemo(
95
120
  () => ({
96
121
  webAnalyticsEventKey: props.eventKey,
@@ -101,12 +126,9 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
101
126
  setBillingAddress,
102
127
  creditCard,
103
128
  setCreditCard,
104
- onPaymentSuccess: (validated: boolean = true) => {
105
- if (validated) {
106
- setStep(SaleTunnelStep.SUCCESS);
107
- } else {
108
- setStep(SaleTunnelStep.NOT_VALIDATED);
109
- }
129
+ nextStep,
130
+ onPaymentSuccess: () => {
131
+ nextStep();
110
132
  WebAnalyticsAPIHandler()?.sendCourseProductEvent(
111
133
  CourseProductEvent.PAYMENT_SUCCEED,
112
134
  props.eventKey,
@@ -154,13 +176,16 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
154
176
 
155
177
  export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
156
178
  const { step } = useSaleTunnelContext();
179
+
157
180
  switch (step) {
158
- case SaleTunnelStep.PAYMENT:
159
- 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} />;
160
187
  case SaleTunnelStep.SUCCESS:
161
188
  return <GenericSaleTunnelSuccessStep {...props} />;
162
- case SaleTunnelStep.NOT_VALIDATED:
163
- return <GenericSaleTunnelNotValidatedStep {...props} />;
164
189
  }
165
190
  throw new Error('Invalid step: ' + step);
166
191
  };
@@ -169,7 +194,7 @@ export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
169
194
  * Steps.
170
195
  */
171
196
 
172
- export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
197
+ export const GenericSaleTunnelInitialStep = (props: GenericSaleTunnelProps) => {
173
198
  const { webAnalyticsEventKey } = useSaleTunnelContext();
174
199
 
175
200
  useEffect(() => {
@@ -184,7 +209,7 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
184
209
  }, []);
185
210
 
186
211
  return (
187
- <Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title} closeOnEsc={false}>
212
+ <Modal {...props} size={ModalSize.EXTRA_LARGE} title={props.product.title}>
188
213
  <div className="sale-tunnel" data-testid="generic-sale-tunnel-payment-step">
189
214
  <div className="sale-tunnel__main">
190
215
  <div className="sale-tunnel__main__column sale-tunnel__main__left ">
@@ -198,14 +223,7 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
198
223
  <SaleTunnelInformation />
199
224
  </div>
200
225
  </div>
201
- <div className="sale-tunnel__footer">
202
- <div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
203
- <Alert type={VariantType.INFO}>
204
- <FormattedMessage {...messages.walkthrough} />
205
- </Alert>
206
- </div>
207
- {props.paymentNode}
208
- </div>
226
+ <div className="sale-tunnel__footer">{props.paymentNode}</div>
209
227
  </div>
210
228
  </Modal>
211
229
  );
@@ -219,10 +237,26 @@ export const GenericSaleTunnelSuccessStep = (props: SaleTunnelProps) => {
219
237
  );
220
238
  };
221
239
 
222
- export const GenericSaleTunnelNotValidatedStep = (props: SaleTunnelProps) => {
240
+ export const GenericSaleTunnelSavePaymentMethodStep = (props: SaleTunnelProps) => {
223
241
  return (
224
- <Modal {...props} size={ModalSize.MEDIUM}>
225
- <SaleTunnelNotValidated closeModal={props.onClose} />
242
+ <Modal {...props} size={ModalSize.LARGE}>
243
+ <SaleTunnelSavePaymentMethod />
226
244
  </Modal>
227
245
  );
228
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,6 +1,5 @@
1
1
  import { defineMessages, FormattedMessage, FormattedNumber } from 'react-intl';
2
2
  import { AddressSelector } from 'components/SaleTunnel/AddressSelector';
3
- import { CreditCardSelector } from 'components/CreditCardSelector';
4
3
  import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
5
4
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
6
5
  import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm';
@@ -25,16 +24,6 @@ const messages = defineMessages({
25
24
  description: 'Label for the full name input',
26
25
  defaultMessage: 'Full name',
27
26
  },
28
- paymentMethodTitle: {
29
- id: 'components.SaleTunnel.CreditCardSelector.title',
30
- description: 'Title for the credit card section',
31
- defaultMessage: 'Payment method',
32
- },
33
- paymentMethodDescription: {
34
- id: 'components.SaleTunnel.CreditCardSelector.description',
35
- description: 'Description for the credit card section',
36
- defaultMessage: 'Choose your payment method or add a new one during the payment.',
37
- },
38
27
  totalInfo: {
39
28
  id: 'components.SaleTunnel.Information.total.info',
40
29
  description: 'Information about the total amount',
@@ -74,9 +63,6 @@ export const SaleTunnelInformation = () => {
74
63
  <Email />
75
64
  </div>
76
65
  </div>
77
- <div>
78
- <CreditCardSelectorWrapper />
79
- </div>
80
66
  <div>
81
67
  <PaymentScheduleBlock />
82
68
  <Total />
@@ -84,22 +70,6 @@ export const SaleTunnelInformation = () => {
84
70
  </div>
85
71
  );
86
72
  };
87
-
88
- const CreditCardSelectorWrapper = () => {
89
- const { creditCard, setCreditCard } = useSaleTunnelContext();
90
- return (
91
- <>
92
- <h4 className="block-title mb-t">
93
- <FormattedMessage {...messages.paymentMethodTitle} />
94
- </h4>
95
- <div className="description mb-s">
96
- <FormattedMessage {...messages.paymentMethodDescription} />
97
- </div>
98
- <CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />
99
- </>
100
- );
101
- };
102
-
103
73
  const Email = () => {
104
74
  const { user } = useSession();
105
75
  const { data: openEdxProfileData } = useOpenEdxProfile({
@@ -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
+ }
@@ -0,0 +1,160 @@
1
+ import { defineMessages, FormattedMessage } from 'react-intl';
2
+ import { Button } from '@openfun/cunningham-react';
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
5
+ import { CreditCard, OrderState } from 'types/Joanie';
6
+ import { Icon, IconTypeEnum } from 'components/Icon';
7
+ import { Payment, PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
8
+ import { useCreditCards } from 'hooks/useCreditCards';
9
+ import { Spinner } from 'components/Spinner';
10
+ import PaymentInterfaces from 'components/PaymentInterfaces';
11
+ import { useOrders } from 'hooks/useOrders';
12
+ import { CreditCardSelector } from 'components/CreditCardSelector';
13
+ import { useJoanieApi } from 'contexts/JoanieApiContext';
14
+
15
+ const messages = defineMessages({
16
+ title: {
17
+ id: 'components.SaleTunnelSavePaymentMethod.title',
18
+ defaultMessage: 'Define a payment method',
19
+ description: 'Content title',
20
+ },
21
+ description: {
22
+ defaultMessage:
23
+ 'This is the last step to validate your subscription, you must define a payment method. This one will be used to debit installments. You will not be charged during this step. Pick an existing payment method or add a new one.',
24
+ description: "Text to explain what the user has to do in the 'save payment method' step",
25
+ id: 'components.SaleTunnelSavePaymentMethod.description',
26
+ },
27
+ cta: {
28
+ defaultMessage: 'Define',
29
+ description: 'Label to the call to action to close sale tunnel',
30
+ id: 'components.SaleTunnelSavePaymentMethod.cta',
31
+ },
32
+ errorAbort: {
33
+ defaultMessage: 'You have aborted the payment.',
34
+ description: 'Error message shown when user aborts the payment.',
35
+ id: 'components.PaymentButton.errorAbort',
36
+ },
37
+ errorDefault: {
38
+ defaultMessage: 'An error occurred during payment. Please retry later.',
39
+ description: 'Error message shown when payment creation request failed.',
40
+ id: 'components.PaymentButton.errorDefault',
41
+ },
42
+ tokenizingPayment: {
43
+ defaultMessage: 'Payment method definition in progress.',
44
+ description: 'Label for screen reader when a credit card is being tokenized.',
45
+ id: 'components.PaymentButton.tokenizingPayment',
46
+ },
47
+ });
48
+
49
+ const SaleTunnelSavePaymentMethod = () => {
50
+ const initialCreditCards = useRef<CreditCard[]>();
51
+ const pollingTimeoutRef = useRef<NodeJS.Timeout>();
52
+ const JoanieApi = useJoanieApi();
53
+ const [payment, setPayment] = useState<Payment>();
54
+ const [error, setError] = useState<string>();
55
+ const creditCards = useCreditCards();
56
+ const orders = useOrders(undefined, { enabled: false });
57
+ const { order, nextStep, creditCard, setCreditCard } = useSaleTunnelContext();
58
+
59
+ const setPaymentMethod = async (creditCardId: string) => {
60
+ orders.methods.set_payment_method(
61
+ { id: order!.id, credit_card_id: creditCardId },
62
+ { onError: () => handleError() },
63
+ );
64
+ };
65
+
66
+ const tokenizePaymentMethod = async () => {
67
+ const data = await creditCards.methods.tokenize();
68
+ setPayment(data);
69
+ setError(undefined);
70
+ };
71
+
72
+ const waitForNewCreditCard = async () => {
73
+ const { results } = await JoanieApi.user.creditCards.get();
74
+ const initialIds = initialCreditCards.current!.map((cc) => cc.id);
75
+ const newCard = results.find((cc) => !initialIds.includes(cc.id));
76
+
77
+ if (!newCard) {
78
+ pollingTimeoutRef.current = setTimeout(waitForNewCreditCard, 1000);
79
+ return;
80
+ }
81
+
82
+ setCreditCard(newCard);
83
+ await setPaymentMethod(newCard.id);
84
+ };
85
+
86
+ const handleError = (message: string = PaymentErrorMessageId.ERROR_DEFAULT) => {
87
+ setError(message);
88
+ setPayment(undefined);
89
+ };
90
+
91
+ useEffect(() => {
92
+ if (!payment) {
93
+ initialCreditCards.current = creditCards.items;
94
+ }
95
+ }, [creditCards]);
96
+
97
+ useEffect(() => {
98
+ if (order?.state !== OrderState.TO_SAVE_PAYMENT_METHOD) {
99
+ nextStep();
100
+ }
101
+ }, [order]);
102
+
103
+ useEffect(
104
+ () => () => {
105
+ clearTimeout(pollingTimeoutRef.current);
106
+ },
107
+ [],
108
+ );
109
+
110
+ return (
111
+ <section
112
+ className="sale-tunnel-step sale-tunnel-step--save-payment-method"
113
+ data-testid="generic-sale-tunnel-save-payment-method-step"
114
+ >
115
+ <header className="sale-tunnel-step__header">
116
+ <Icon name={IconTypeEnum.CREDIT_CARD} />
117
+ <h3 className="sale-tunnel-step__title">
118
+ <FormattedMessage {...messages.title} />
119
+ </h3>
120
+ </header>
121
+ <div className="sale-tunnel-step__content">
122
+ <p className="mb-s">
123
+ <FormattedMessage {...messages.description} />
124
+ </p>
125
+ <CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />
126
+ </div>
127
+ <footer className="sale-tunnel-step__footer">
128
+ <Button
129
+ size="medium"
130
+ disabled={!!payment || orders.states.settingPaymentMethod}
131
+ onClick={creditCard ? () => setPaymentMethod(creditCard.id) : tokenizePaymentMethod}
132
+ >
133
+ {payment || orders.states.settingPaymentMethod ? (
134
+ <Spinner size="small">
135
+ <span id="tokenizing-payment">
136
+ <FormattedMessage {...messages.tokenizingPayment} />
137
+ </span>
138
+ </Spinner>
139
+ ) : (
140
+ <FormattedMessage {...messages.cta} />
141
+ )}
142
+ </Button>
143
+ {error && (
144
+ <p className="payment-button__error">
145
+ {messages.hasOwnProperty(error) ? (
146
+ <FormattedMessage {...messages[error as PaymentErrorMessageId]} />
147
+ ) : (
148
+ error
149
+ )}
150
+ </p>
151
+ )}
152
+ {payment && (
153
+ <PaymentInterfaces {...payment} onSuccess={waitForNewCreditCard} onError={handleError} />
154
+ )}
155
+ </footer>
156
+ </section>
157
+ );
158
+ };
159
+
160
+ export default SaleTunnelSavePaymentMethod;
@@ -5,72 +5,58 @@ import { SuccessIcon } from 'components/SuccessIcon';
5
5
  import { getDashboardBasename } from 'widgets/Dashboard/hooks/useDashboardRouter/getDashboardBasename';
6
6
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
7
7
  import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
8
+ import { ProductType } from 'types/Joanie';
8
9
 
9
10
  const messages = defineMessages({
10
11
  congratulations: {
11
- defaultMessage: 'Congratulations!',
12
+ defaultMessage: 'Subscription confirmed!',
12
13
  description: 'Text displayed to thank user for his order',
13
14
  id: 'components.SaleTunnelSuccess.congratulations',
14
15
  },
15
16
  successMessage: {
16
- defaultMessage: 'Your order has been successfully created.',
17
+ defaultMessage: 'Your order has been successfully registered.',
17
18
  description: 'Message to confirm that order has been created',
18
19
  id: 'components.SaleTunnelSuccess.successMessage',
19
20
  },
20
21
  successDetailMessage: {
21
- defaultMessage: 'You will receive your invoice by email in a few moments.',
22
- description: "Text to remind that order's invoice will be send by email soon",
23
- id: 'components.SaleTunnelSuccess.successDetailMessage',
24
- },
25
- successDetailSignatureMessage: {
26
22
  defaultMessage:
27
- 'In order to enroll to course runs you first need to sign the training contract.',
28
- description: 'Text to remind that order needs to be signed',
29
- id: 'components.SaleTunnelSuccess.successDetailSignatureMessage',
23
+ 'You will be able to start your training once the first installment will be paid.',
24
+ description: 'Text to explain when the user will be able to start its training.',
25
+ id: 'components.SaleTunnelSuccess.successDetailMessage',
30
26
  },
31
27
  cta: {
32
- defaultMessage: 'Start this course now!',
28
+ defaultMessage: 'Close',
33
29
  description: 'Label to the call to action to close sale tunnel',
34
30
  id: 'components.SaleTunnelSuccess.cta',
35
31
  },
36
- ctaSignature: {
37
- defaultMessage: 'Sign the training contract',
38
- description: 'Label to the call to action to close sale tunnel if there is a pending signature',
39
- id: 'components.SaleTunnelSuccess.ctaSignature',
40
- },
41
32
  });
42
33
 
43
34
  export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) => {
44
35
  const intl = useIntl();
45
36
  const { order, product } = useSaleTunnelContext();
37
+
46
38
  return (
47
- <section className="sale-tunnel-end" data-testid="generic-sale-tunnel-success-step">
48
- <header className="sale-tunnel-end__header">
39
+ <section className="sale-tunnel-step" data-testid="generic-sale-tunnel-success-step">
40
+ <header className="sale-tunnel-step__header">
49
41
  <SuccessIcon />
50
- <h3 className="sale-tunnel-end__title">
42
+ <h3 className="sale-tunnel-step__title">
51
43
  <FormattedMessage {...messages.congratulations} />
52
44
  </h3>
53
45
  </header>
54
- <p className="sale-tunnel-end__content">
46
+ <p className="sale-tunnel-step__content">
55
47
  <FormattedMessage {...messages.successMessage} />
56
48
  <br />
57
49
  <FormattedMessage {...messages.successDetailMessage} />
58
- {product.contract_definition && (
59
- <>
60
- <br />
61
- <FormattedMessage {...messages.successDetailSignatureMessage} />
62
- </>
63
- )}
64
50
  </p>
65
- <footer className="sale-tunnel-end__footer">
66
- {product.contract_definition ? (
51
+ <footer className="sale-tunnel-step__footer">
52
+ {product.type === ProductType.CREDENTIAL ? (
67
53
  <Button
68
54
  href={
69
55
  getDashboardBasename(intl.locale) +
70
56
  generatePath(LearnerDashboardPaths.ORDER, { orderId: order!.id })
71
57
  }
72
58
  >
73
- <FormattedMessage {...messages.ctaSignature} />
59
+ <FormattedMessage {...messages.cta} />
74
60
  </Button>
75
61
  ) : (
76
62
  <Button onClick={closeModal}>
@@ -18,6 +18,11 @@ export const SaleTunnelSponsors = () => {
18
18
  const {
19
19
  props: { organizations },
20
20
  } = useSaleTunnelContext();
21
+
22
+ if (!organizations || organizations.length === 0) {
23
+ return null;
24
+ }
25
+
21
26
  return (
22
27
  <>
23
28
  <h3 className="block-title">
@@ -0,0 +1,7 @@
1
+ .subscription-button {
2
+ &__error {
3
+ color: r-theme-val(form, input-color-error);
4
+ margin-top: 0.5rem;
5
+ margin-bottom: 0;
6
+ }
7
+ }