richie-education 2.28.2-dev39 → 2.28.2-dev58

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 (102) hide show
  1. package/.eslintrc.json +11 -2
  2. package/i18n/locales/ar-SA.json +209 -125
  3. package/i18n/locales/es-ES.json +210 -126
  4. package/i18n/locales/fa-IR.json +209 -125
  5. package/i18n/locales/fr-CA.json +209 -125
  6. package/i18n/locales/fr-FR.json +209 -125
  7. package/i18n/locales/ko-KR.json +209 -125
  8. package/i18n/locales/pt-PT.json +212 -128
  9. package/i18n/locales/ru-RU.json +209 -125
  10. package/i18n/locales/vi-VN.json +209 -125
  11. package/js/api/joanie.ts +14 -17
  12. package/js/api/lms/dummy.ts +1 -12
  13. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  14. package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
  15. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  16. package/js/components/ContractFrame/_styles.scss +6 -14
  17. package/js/components/CreditCardSelector/index.spec.tsx +7 -7
  18. package/js/components/CreditCardSelector/index.tsx +2 -2
  19. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  20. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  21. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  22. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  23. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  24. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  25. package/js/components/PaymentInterfaces/types.ts +5 -2
  26. package/js/components/PurchaseButton/index.spec.tsx +69 -37
  27. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  28. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  29. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  30. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  31. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  32. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  33. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  34. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  35. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  36. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  37. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
  38. package/js/components/SaleTunnel/_styles.scss +10 -1
  39. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  40. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  41. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  42. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  43. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  44. package/js/components/SignContractButton/index.spec.tsx +16 -20
  45. package/js/components/SignContractButton/index.tsx +3 -1
  46. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  47. package/js/hooks/useCreditCards/index.ts +49 -11
  48. package/js/hooks/useOrders/index.spec.tsx +322 -0
  49. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  50. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  51. package/js/hooks/useProductOrder/index.tsx +2 -2
  52. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  53. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  54. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  55. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  56. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  57. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  58. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  59. package/js/settings/settings.test.ts +11 -2
  60. package/js/translations/ar-SA.json +1 -1
  61. package/js/translations/es-ES.json +1 -1
  62. package/js/translations/fa-IR.json +1 -1
  63. package/js/translations/fr-CA.json +1 -1
  64. package/js/translations/fr-FR.json +1 -1
  65. package/js/translations/ko-KR.json +1 -1
  66. package/js/translations/pt-PT.json +1 -1
  67. package/js/translations/ru-RU.json +1 -1
  68. package/js/translations/vi-VN.json +1 -1
  69. package/js/types/Joanie.ts +49 -34
  70. package/js/utils/OrderHelper/index.ts +38 -42
  71. package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
  72. package/js/utils/test/factories/joanie.ts +36 -51
  73. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  74. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  75. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  76. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  77. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  78. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  79. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  80. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  81. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +4 -6
  82. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  83. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  84. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  85. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  86. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  87. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  88. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  89. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  90. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  91. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  92. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  93. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  94. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  95. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  96. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  97. package/package.json +27 -27
  98. package/scss/components/_index.scss +2 -1
  99. package/js/components/PaymentButton/_styles.scss +0 -27
  100. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  101. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  102. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
@@ -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
+ }
@@ -0,0 +1,202 @@
1
+ import { useMemo, useEffect, useState } from 'react';
2
+ import { Alert, Button, VariantType } from '@openfun/cunningham-react';
3
+ import { defineMessages, FormattedMessage } from 'react-intl';
4
+ import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
5
+ import { useOrders } from 'hooks/useOrders';
6
+ import { OrderCreationPayload } from 'types/Joanie';
7
+ import { useMatchMediaLg } from 'hooks/useMatchMedia';
8
+ import { SubscriptionErrorMessageId } from 'components/PaymentInterfaces/types';
9
+ import { HttpError } from 'utils/errors/HttpError';
10
+ import { Spinner } from 'components/Spinner';
11
+
12
+ const messages = defineMessages({
13
+ subscribe: {
14
+ id: 'components.SaleTunnel.SubscriptionButton.subscribe',
15
+ defaultMessage: 'Subscribe',
16
+ description: 'Label of the button to subscribe to a product.',
17
+ },
18
+ walkthroughToSignAndSavePayment: {
19
+ id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSignAndSavePayment',
20
+ defaultMessage:
21
+ 'To enroll in the training, you will first be invited to sign the training agreement and then to define a payment method.',
22
+ description:
23
+ 'Message explaining the subscription process with a training agreement to sign and a payment method to set.',
24
+ },
25
+ walkthroughToSign: {
26
+ id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSign',
27
+ defaultMessage:
28
+ 'To enroll in the training, you will be invited to sign the training agreement.',
29
+ description:
30
+ 'Message explaining the subscription process with only a training agreement to sign.',
31
+ },
32
+ walkthroughToSavePayment: {
33
+ id: 'components.SaleTunnel.SubscriptionButton.walkthroughToSavePayment',
34
+ defaultMessage: 'To enroll in the training, you will be invited to define a payment method.',
35
+ description: 'Message explaining the subscription process with only a payment method to set.',
36
+ },
37
+ errorDefault: {
38
+ defaultMessage: 'An error occurred during order creation. Please retry later.',
39
+ description: 'Error message shown when order creation request failed.',
40
+ id: 'components.SubscriptionButton.errorDefault',
41
+ },
42
+ errorFullProduct: {
43
+ defaultMessage: 'There are no more places available for this product.',
44
+ description:
45
+ 'Error message shown when order creation request failed because there is no remaining available seat for the product.',
46
+ id: 'components.SubscriptionButton.errorFullProduct',
47
+ },
48
+ errorAddress: {
49
+ defaultMessage: 'You must have a billing address.',
50
+ description: "Error message shown when the user didn't select a billing address.",
51
+ id: 'components.SubscriptionButton.errorAddress',
52
+ },
53
+ orderCreationInProgress: {
54
+ defaultMessage: 'Order creation in progress',
55
+ description: 'Label for screen reader when an order creation is in progress.',
56
+ id: 'components.SubscriptionButton.orderCreationInProgress',
57
+ },
58
+ });
59
+
60
+ enum ComponentStates {
61
+ IDLE = 'idle',
62
+ LOADING = 'loading',
63
+ ERROR = 'error',
64
+ }
65
+
66
+ interface Props {
67
+ buildOrderPayload: (
68
+ payload: Pick<OrderCreationPayload, 'product_id' | 'billing_address' | 'order_group_id'>,
69
+ ) => OrderCreationPayload;
70
+ }
71
+
72
+ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
73
+ const {
74
+ order,
75
+ creditCard,
76
+ billingAddress,
77
+ product,
78
+ nextStep,
79
+ runSubmitCallbacks,
80
+ props: saleTunnelProps,
81
+ } = useSaleTunnelContext();
82
+ const { methods: orderMethods } = useOrders(undefined, { enabled: false });
83
+ const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
84
+ const [error, setError] = useState<SubscriptionErrorMessageId | string>();
85
+ const isMobile = useMatchMediaLg();
86
+
87
+ const handleError = (
88
+ messageId: SubscriptionErrorMessageId | string = SubscriptionErrorMessageId.ERROR_DEFAULT,
89
+ ) => {
90
+ setState(ComponentStates.ERROR);
91
+ setError(messageId);
92
+ };
93
+
94
+ const createOrder = async () => {
95
+ setState(ComponentStates.LOADING);
96
+
97
+ try {
98
+ await runSubmitCallbacks();
99
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
100
+ } catch (_error) {
101
+ setState(ComponentStates.IDLE);
102
+ return;
103
+ }
104
+
105
+ if (!billingAddress) {
106
+ handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
107
+ return;
108
+ }
109
+
110
+ const payload = buildOrderPayload({
111
+ product_id: product.id,
112
+ billing_address: billingAddress!,
113
+ order_group_id: saleTunnelProps.orderGroup?.id,
114
+ });
115
+
116
+ orderMethods.create(payload, {
117
+ onError: async (createOrderError: HttpError) => {
118
+ if (createOrderError.responseBody) {
119
+ const responseErrors = await createOrderError.responseBody;
120
+ if ('max_validated_orders' in responseErrors) {
121
+ handleError(SubscriptionErrorMessageId.ERROR_FULL_PRODUCT);
122
+ }
123
+ }
124
+ handleError();
125
+ },
126
+ });
127
+ };
128
+
129
+ const walkthroughMessages = useMemo(() => {
130
+ if (product.contract_definition && product.price > 0) {
131
+ return messages.walkthroughToSignAndSavePayment;
132
+ } else if (product.contract_definition && product.price === 0) {
133
+ return messages.walkthroughToSign;
134
+ } else if (!product.contract_definition && product.price > 0) {
135
+ return messages.walkthroughToSavePayment;
136
+ }
137
+ }, [product, creditCard]);
138
+
139
+ useEffect(() => {
140
+ if (order) nextStep();
141
+ }, [order]);
142
+
143
+ useEffect(() => {
144
+ if (error && [ComponentStates.IDLE, ComponentStates.LOADING].includes(state)) {
145
+ setError(undefined);
146
+ }
147
+ if (state === ComponentStates.ERROR) {
148
+ document.querySelector<HTMLElement>('#sale-tunnel-subscription-error')?.focus();
149
+ }
150
+ }, [state]);
151
+
152
+ return (
153
+ <>
154
+ <div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
155
+ {walkthroughMessages && (
156
+ <Alert type={VariantType.INFO}>
157
+ <FormattedMessage
158
+ {...walkthroughMessages}
159
+ values={{ credictCarNumbers: creditCard?.last_numbers }}
160
+ />
161
+ </Alert>
162
+ )}
163
+ </div>
164
+ <Button
165
+ onClick={createOrder}
166
+ fullWidth={isMobile}
167
+ disabled={state === ComponentStates.LOADING}
168
+ {...(state === ComponentStates.ERROR && {
169
+ 'aria-describedby': 'sale-tunnel-payment-error',
170
+ })}
171
+ >
172
+ {state === ComponentStates.LOADING ? (
173
+ <Spinner theme="light" aria-labelledby="order-creation-in-progress">
174
+ <span id="order-creation-in-progress">
175
+ <FormattedMessage {...messages.orderCreationInProgress} />
176
+ </span>
177
+ </Spinner>
178
+ ) : (
179
+ <FormattedMessage {...messages.subscribe} />
180
+ )}
181
+ </Button>
182
+ {state === ComponentStates.ERROR && (
183
+ <p className="subscription-button__error" id="sale-tunnel-subscription-error" tabIndex={-1}>
184
+ {!error || messages.hasOwnProperty(error) ? (
185
+ <FormattedMessage
186
+ {...messages[
187
+ (error as Exclude<
188
+ SubscriptionErrorMessageId,
189
+ SubscriptionErrorMessageId.ERROR_ABORT
190
+ >) || SubscriptionErrorMessageId.ERROR_DEFAULT
191
+ ]}
192
+ />
193
+ ) : (
194
+ error
195
+ )}
196
+ </p>
197
+ )}
198
+ </>
199
+ );
200
+ };
201
+
202
+ export default SubscriptionButton;
@@ -105,14 +105,23 @@
105
105
  text-align: left;
106
106
  }
107
107
 
108
- .sale-tunnel-end {
108
+ .sale-tunnel-step {
109
109
  align-items: center;
110
110
  display: flex;
111
111
  flex-direction: column;
112
112
 
113
113
  &__header {
114
+ align-items: center;
115
+ display: flex;
116
+ flex-direction: column;
114
117
  margin-bottom: 0;
115
118
  text-align: center;
119
+
120
+ & > .icon {
121
+ --size: 4.5rem;
122
+ height: var(--size);
123
+ width: var(--size);
124
+ }
116
125
  }
117
126
 
118
127
  &__title {
@@ -1,77 +0,0 @@
1
- import { Checkbox } from '@openfun/cunningham-react';
2
- import { useState } from 'react';
3
- import { defineMessages, useIntl } from 'react-intl';
4
- import { Product } from 'types/Joanie';
5
- import context from 'utils/context';
6
- import { PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
7
-
8
- const messages = defineMessages({
9
- termsMessage: {
10
- defaultMessage: 'By checking this box, you accept the ',
11
- description: 'Message next to the checkbox in order to accept the terms',
12
- id: 'components.PaymentButton.termsMessage',
13
- },
14
- termsMessageLink: {
15
- defaultMessage: 'General Terms of Sale',
16
- description: 'Clickable link included in the terms message',
17
- id: 'components.SaleTunnelStepPayment.termsMessageLink',
18
- },
19
- termsMessageLinkTitle: {
20
- defaultMessage: 'Open a preview of the General Terms of Sale',
21
- description: 'Title of clickable link included in the terms message',
22
- id: 'components.SaleTunnelStepPayment.termsMessageLinkTitle',
23
- },
24
- });
25
-
26
- export const useTerms = ({
27
- product,
28
- onError,
29
- error,
30
- }: {
31
- product: Product;
32
- onError: (error: PaymentErrorMessageId | string) => void;
33
- error?: PaymentErrorMessageId | string;
34
- }) => {
35
- const intl = useIntl();
36
- const [termsAccepted, setTermsAccepted] = useState(false);
37
- const validateTerms = () => {
38
- if (!product.contract_definition) {
39
- return;
40
- }
41
- if (!termsAccepted) {
42
- onError(PaymentErrorMessageId.ERROR_TERMS);
43
- }
44
- };
45
-
46
- return {
47
- termsAccepted: termsAccepted || !product.contract_definition,
48
- validateTerms,
49
- renderTermsCheckbox: () => {
50
- if (!product.contract_definition) {
51
- return null;
52
- }
53
- return (
54
- <section className="payment-button__terms">
55
- <Checkbox
56
- label={
57
- <>
58
- {intl.formatMessage(messages.termsMessage)}{' '}
59
- <a
60
- href={context.site_urls.terms_and_conditions ?? '#'}
61
- target="_blank"
62
- rel="noopener noreferrer"
63
- title={intl.formatMessage(messages.termsMessageLinkTitle)}
64
- >
65
- {intl.formatMessage(messages.termsMessageLink)}
66
- </a>
67
- </>
68
- }
69
- onChange={(e) => setTermsAccepted(e.target.checked)}
70
- checked={termsAccepted}
71
- state={error === PaymentErrorMessageId.ERROR_TERMS ? 'error' : 'default'}
72
- />
73
- </section>
74
- );
75
- },
76
- };
77
- };
@@ -1,5 +1,6 @@
1
1
  import fetchMock from 'fetch-mock';
2
2
  import { act, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import queryString from 'query-string';
3
4
  import {
4
5
  RichieContextFactory as mockRichieContextFactory,
5
6
  PacedCourseFactory,
@@ -8,13 +9,13 @@ import {
8
9
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
9
10
  import {
10
11
  AddressFactory,
11
- CredentialOrderWithPaymentFactory,
12
+ CredentialOrderFactory,
12
13
  CredentialProductFactory,
13
14
  OrderGroupFactory,
14
15
  } from 'utils/test/factories/joanie';
15
16
  import type * as Joanie from 'types/Joanie';
16
17
  import { Maybe } from 'types/utils';
17
- import { OrderCredentialCreationPayload } from 'types/Joanie';
18
+ import { NOT_CANCELED_ORDER_STATES, OrderCredentialCreationPayload } from 'types/Joanie';
18
19
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
19
20
  import { render } from 'utils/test/render';
20
21
  import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
@@ -86,26 +87,23 @@ describe('SaleTunnel / Credential', () => {
86
87
  const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
87
88
 
88
89
  let createOrderPayload: Maybe<OrderCredentialCreationPayload>;
89
- const { payment_info: paymentInfo, ...order } = CredentialOrderWithPaymentFactory().one();
90
+ const order = CredentialOrderFactory({ order_group_id: orderGroup.id }).one();
91
+ const orderQueryParameters = {
92
+ course_code: course.code,
93
+ product_id: product.id,
94
+ state: NOT_CANCELED_ORDER_STATES,
95
+ };
96
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`;
90
97
  fetchMock
91
- .get(
92
- `https://joanie.endpoint/api/v1.0/orders/?course_code=${course.code}&product_id=${product.id}&state=pending&state=validated&state=submitted`,
93
- [],
94
- )
98
+ .get(url, [])
95
99
  .get(
96
100
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
97
101
  [],
98
102
  )
99
- .post('https://joanie.endpoint/api/v1.0/orders/', (url, { body }) => {
103
+ .post('https://joanie.endpoint/api/v1.0/orders/', (_, { body }) => {
100
104
  createOrderPayload = JSON.parse(body as any);
101
105
  return order;
102
106
  })
103
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
104
- paymentInfo,
105
- })
106
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
107
- ...order,
108
- })
109
107
  .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
110
108
  overwriteRoutes: true,
111
109
  });
@@ -121,13 +119,6 @@ describe('SaleTunnel / Credential', () => {
121
119
  name: `Subscribe`,
122
120
  }) as HTMLButtonElement;
123
121
 
124
- const $terms = screen.getByLabelText(
125
- 'By checking this box, you accept the General Terms of Sale',
126
- );
127
- await act(async () => {
128
- fireEvent.click($terms);
129
- });
130
-
131
122
  // - Payment button should not be disabled.
132
123
  expect($button.disabled).toBe(false);
133
124