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
@@ -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}>
@@ -1,15 +1,14 @@
1
1
  .sale-tunnel__sponsors {
2
2
  margin-top: 8px;
3
3
  display: flex;
4
- gap: 8px;
5
- justify-content: center;
4
+ gap: 16px;
5
+ justify-content: flex-start;
6
+ align-items: center;
6
7
  overflow: hidden;
7
8
  flex-wrap: wrap;
8
9
 
9
10
  img {
10
- height: 50px;
11
- padding: 8px;
12
- width: 130px;
11
+ width: 90px;
13
12
  object-fit: contain;
14
13
  }
15
14
  }
@@ -1,20 +1,48 @@
1
+ import { defineMessages, FormattedMessage } from 'react-intl';
1
2
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
2
- import { DashboardAvatar } from 'widgets/Dashboard/components/DashboardAvatar';
3
+ import {
4
+ DashboardAvatar,
5
+ DashboardAvatarVariantEnum,
6
+ } from 'widgets/Dashboard/components/DashboardAvatar';
7
+ import { Organization } from 'types/Joanie';
8
+
9
+ const messages = defineMessages({
10
+ blockTitle: {
11
+ id: 'components.SaleTunnel.Sponsors.SaleTunnelSponsors.blockTitle',
12
+ defaultMessage: 'University',
13
+ description: 'Title for the universities section in the sale tunnel',
14
+ },
15
+ });
3
16
 
4
17
  export const SaleTunnelSponsors = () => {
5
18
  const {
6
19
  props: { organizations },
7
20
  } = useSaleTunnelContext();
21
+
22
+ if (!organizations || organizations.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <>
28
+ <h3 className="block-title">
29
+ <FormattedMessage {...messages.blockTitle} />
30
+ </h3>
31
+ <div className="sale-tunnel__sponsors">{organizations?.map(OrganizationLogo)}</div>
32
+ </>
33
+ );
34
+ };
35
+
36
+ const OrganizationLogo = (organization: Organization) => {
37
+ if (organization.logo) {
38
+ return <img key={organization.id} src={organization.logo!.src} alt={organization.title} />;
39
+ }
40
+
8
41
  return (
9
- <div className="sale-tunnel__sponsors">
10
- {organizations?.map((organization) => {
11
- if (organization.logo) {
12
- return (
13
- <img key={organization.id} src={organization.logo!.src} alt={organization.title} />
14
- );
15
- }
16
- return <DashboardAvatar key={organization.id} title={organization.title} />;
17
- })}
18
- </div>
42
+ <DashboardAvatar
43
+ key={organization.id}
44
+ title={organization.title}
45
+ variant={DashboardAvatarVariantEnum.SQUARE}
46
+ />
19
47
  );
20
48
  };
@@ -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,201 @@
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
+ } catch (e) {
100
+ setState(ComponentStates.IDLE);
101
+ return;
102
+ }
103
+
104
+ if (!billingAddress) {
105
+ handleError(SubscriptionErrorMessageId.ERROR_ADDRESS);
106
+ return;
107
+ }
108
+
109
+ const payload = buildOrderPayload({
110
+ product_id: product.id,
111
+ billing_address: billingAddress!,
112
+ order_group_id: saleTunnelProps.orderGroup?.id,
113
+ });
114
+
115
+ orderMethods.create(payload, {
116
+ onError: async (createOrderError: HttpError) => {
117
+ if (createOrderError.responseBody) {
118
+ const responseErrors = await createOrderError.responseBody;
119
+ if ('max_validated_orders' in responseErrors) {
120
+ handleError(SubscriptionErrorMessageId.ERROR_FULL_PRODUCT);
121
+ }
122
+ }
123
+ handleError();
124
+ },
125
+ });
126
+ };
127
+
128
+ const walkthroughMessages = useMemo(() => {
129
+ if (product.contract_definition && product.price > 0) {
130
+ return messages.walkthroughToSignAndSavePayment;
131
+ } else if (product.contract_definition && product.price === 0) {
132
+ return messages.walkthroughToSign;
133
+ } else if (!product.contract_definition && product.price > 0) {
134
+ return messages.walkthroughToSavePayment;
135
+ }
136
+ }, [product, creditCard]);
137
+
138
+ useEffect(() => {
139
+ if (order) nextStep();
140
+ }, [order]);
141
+
142
+ useEffect(() => {
143
+ if (error && [ComponentStates.IDLE, ComponentStates.LOADING].includes(state)) {
144
+ setError(undefined);
145
+ }
146
+ if (state === ComponentStates.ERROR) {
147
+ document.querySelector<HTMLElement>('#sale-tunnel-subscription-error')?.focus();
148
+ }
149
+ }, [state]);
150
+
151
+ return (
152
+ <>
153
+ <div style={{ maxWidth: '680px' }} className="mb-s" data-testid="walkthrough-banner">
154
+ {walkthroughMessages && (
155
+ <Alert type={VariantType.INFO}>
156
+ <FormattedMessage
157
+ {...walkthroughMessages}
158
+ values={{ credictCarNumbers: creditCard?.last_numbers }}
159
+ />
160
+ </Alert>
161
+ )}
162
+ </div>
163
+ <Button
164
+ onClick={createOrder}
165
+ fullWidth={isMobile}
166
+ disabled={state === ComponentStates.LOADING}
167
+ {...(state === ComponentStates.ERROR && {
168
+ 'aria-describedby': 'sale-tunnel-payment-error',
169
+ })}
170
+ >
171
+ {state === ComponentStates.LOADING ? (
172
+ <Spinner theme="light" aria-labelledby="order-creation-in-progress">
173
+ <span id="order-creation-in-progress">
174
+ <FormattedMessage {...messages.orderCreationInProgress} />
175
+ </span>
176
+ </Spinner>
177
+ ) : (
178
+ <FormattedMessage {...messages.subscribe} />
179
+ )}
180
+ </Button>
181
+ {state === ComponentStates.ERROR && (
182
+ <p className="subscription-button__error" id="sale-tunnel-subscription-error" tabIndex={-1}>
183
+ {!error || messages.hasOwnProperty(error) ? (
184
+ <FormattedMessage
185
+ {...messages[
186
+ (error as Exclude<
187
+ SubscriptionErrorMessageId,
188
+ SubscriptionErrorMessageId.ERROR_ABORT
189
+ >) || SubscriptionErrorMessageId.ERROR_DEFAULT
190
+ ]}
191
+ />
192
+ ) : (
193
+ error
194
+ )}
195
+ </p>
196
+ )}
197
+ </>
198
+ );
199
+ };
200
+
201
+ export default SubscriptionButton;
@@ -33,13 +33,15 @@
33
33
  flex: 1;
34
34
  overflow: hidden;
35
35
  }
36
+
37
+ &__column {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--c--theme--spacings--b);
41
+ }
36
42
  }
37
43
 
38
44
  &__information {
39
- display: flex;
40
- flex-direction: column;
41
- gap: var(--c--theme--spacings--b);
42
-
43
45
  &__billing-address {
44
46
  display: flex;
45
47
  align-items: center;
@@ -103,14 +105,23 @@
103
105
  text-align: left;
104
106
  }
105
107
 
106
- .sale-tunnel-end {
108
+ .sale-tunnel-step {
107
109
  align-items: center;
108
110
  display: flex;
109
111
  flex-direction: column;
110
112
 
111
113
  &__header {
114
+ align-items: center;
115
+ display: flex;
116
+ flex-direction: column;
112
117
  margin-bottom: 0;
113
118
  text-align: center;
119
+
120
+ & > .icon {
121
+ --size: 4.5rem;
122
+ height: var(--size);
123
+ width: var(--size);
124
+ }
114
125
  }
115
126
 
116
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
- };