richie-education 2.29.2 → 2.29.3-dev48

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 (127) hide show
  1. package/i18n/locales/ar-SA.json +193 -125
  2. package/i18n/locales/es-ES.json +193 -125
  3. package/i18n/locales/fa-IR.json +193 -125
  4. package/i18n/locales/fr-CA.json +193 -125
  5. package/i18n/locales/fr-FR.json +193 -125
  6. package/i18n/locales/ko-KR.json +193 -125
  7. package/i18n/locales/pt-PT.json +193 -125
  8. package/i18n/locales/ru-RU.json +193 -125
  9. package/i18n/locales/vi-VN.json +193 -125
  10. package/js/api/joanie.ts +42 -17
  11. package/js/api/lms/dummy.ts +1 -12
  12. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  13. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  14. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  15. package/js/components/ContractFrame/_styles.scss +6 -14
  16. package/js/components/ContractStatus/index.spec.tsx +1 -1
  17. package/js/components/ContractStatus/index.tsx +1 -1
  18. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
  19. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +19 -26
  20. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  21. package/js/components/Form/utils.ts +2 -1
  22. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  23. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  24. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  25. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  26. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  27. package/js/components/PaymentInterfaces/types.ts +5 -2
  28. package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
  29. package/js/components/PaymentScheduleGrid/index.tsx +50 -70
  30. package/js/components/PurchaseButton/index.spec.tsx +84 -37
  31. package/js/components/PurchaseButton/index.tsx +9 -27
  32. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -2
  33. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  34. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  35. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +105 -48
  36. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
  37. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  38. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +157 -0
  39. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  40. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
  41. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
  42. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  43. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
  44. package/js/components/SaleTunnel/_styles.scss +16 -5
  45. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  46. package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
  47. package/js/components/SaleTunnel/index.full-process.spec.tsx +114 -48
  48. package/js/components/SaleTunnel/index.spec.tsx +333 -717
  49. package/js/components/SaleTunnel/index.tsx +2 -1
  50. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  51. package/js/components/SignContractButton/index.spec.tsx +16 -20
  52. package/js/components/SignContractButton/index.tsx +3 -1
  53. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  54. package/js/hooks/useCreditCards/index.ts +49 -11
  55. package/js/hooks/useOrders/index.spec.tsx +322 -0
  56. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  57. package/js/hooks/usePaymentSchedule.tsx +23 -0
  58. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  59. package/js/hooks/useProductOrder/index.tsx +2 -2
  60. package/js/hooks/useResources/useResourcesRoot.ts +4 -3
  61. package/js/index.tsx +2 -0
  62. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  63. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  64. package/js/pages/DashboardOrderLayout/index.spec.tsx +10 -1
  65. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  66. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  67. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  68. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  69. package/js/settings/settings.prod.ts +0 -2
  70. package/js/settings/settings.test.ts +16 -2
  71. package/js/translations/ar-SA.json +1 -1
  72. package/js/translations/es-ES.json +1 -1
  73. package/js/translations/fa-IR.json +1 -1
  74. package/js/translations/fr-CA.json +1 -1
  75. package/js/translations/fr-FR.json +1 -1
  76. package/js/translations/ko-KR.json +1 -1
  77. package/js/translations/pt-PT.json +1 -1
  78. package/js/translations/ru-RU.json +1 -1
  79. package/js/translations/vi-VN.json +1 -1
  80. package/js/types/Joanie.ts +81 -32
  81. package/js/utils/OrderHelper/index.ts +46 -38
  82. package/js/utils/PaymentScheduleHelper/index.spec.tsx +78 -0
  83. package/js/utils/PaymentScheduleHelper/index.ts +30 -0
  84. package/js/utils/ProductHelper/index.spec.ts +322 -166
  85. package/js/utils/ProductHelper/index.ts +32 -0
  86. package/js/utils/test/factories/joanie.ts +72 -69
  87. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +2 -2
  88. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  89. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +23 -0
  90. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +23 -1
  91. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +145 -34
  92. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +175 -37
  93. package/js/widgets/Dashboard/components/DashboardItem/Order/CertificateItem/index.tsx +51 -0
  94. package/js/widgets/Dashboard/components/DashboardItem/Order/ContractItem/index.tsx +52 -0
  95. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemNotResumable.spec.tsx +109 -0
  96. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +219 -5
  97. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +111 -245
  98. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +61 -8
  99. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +17 -7
  100. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemSavePaymentMethod.spec.tsx +116 -0
  101. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +175 -0
  102. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
  103. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +127 -0
  104. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +216 -0
  105. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  106. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
  107. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
  108. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  109. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  110. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +150 -0
  111. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +32 -14
  112. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  113. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  114. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  115. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  116. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  117. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  118. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  119. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  120. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  121. package/package.json +45 -44
  122. package/scss/components/_index.scss +4 -2
  123. package/js/components/PaymentButton/_styles.scss +0 -27
  124. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -340
  125. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  126. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
  127. /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Test suite for DashboardItem component with an order in the state TO_SAVE_PAYMENT_METHOD.
3
+ */
4
+ import fetchMock from 'fetch-mock';
5
+ import { screen } from '@testing-library/react';
6
+ import { within } from '@testing-library/dom';
7
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
+ import { CredentialOrderFactory } from 'utils/test/factories/joanie';
9
+ import { OrderState } from 'types/Joanie';
10
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
11
+ import { render } from 'utils/test/render';
12
+ import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
13
+ import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
14
+ import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
15
+ import { mockCourseProductWithOrder } from 'utils/test/mockCourseProductWithOrder';
16
+ import { expectBannerError, expectNoBannerError } from 'utils/test/expectBanner';
17
+
18
+ jest.mock('utils/context', () => ({
19
+ __esModule: true,
20
+ default: mockRichieContextFactory({
21
+ authentication: { backend: 'fonzie', endpoint: 'https://demo.endpoint' },
22
+ joanie_backend: { endpoint: 'https://joanie.endpoint' },
23
+ }).one(),
24
+ }));
25
+
26
+ describe('DashboardItem / Save Payment Methode State', () => {
27
+ setupJoanieSession();
28
+ beforeEach(() => {
29
+ fetchMock.get(
30
+ 'begin:https://joanie.endpoint/api/v1.0/enrollments/',
31
+ { results: [], next: null, previous: null, count: null },
32
+ { overwriteRoutes: true },
33
+ );
34
+ });
35
+ afterEach(() => {
36
+ fetchMock.restore();
37
+ });
38
+
39
+ describe('non-writable', () => {
40
+ it('renders elements to explain that a payment method is missing', async () => {
41
+ const order = CredentialOrderFactory({
42
+ state: OrderState.TO_SAVE_PAYMENT_METHOD,
43
+ }).one();
44
+
45
+ fetchMock.get('begin:https://joanie.endpoint/api/v1.0/orders/', {
46
+ results: [order],
47
+ next: null,
48
+ previous: null,
49
+ count: null,
50
+ });
51
+
52
+ const { product } = mockCourseProductWithOrder(order);
53
+
54
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
55
+ wrapper: BaseJoanieAppWrapper,
56
+ });
57
+
58
+ const dashboardItem = await screen.findByTestId(`dashboard-item-order-${order.id}`);
59
+ within(dashboardItem).getByRole('heading', { level: 5, name: product.title });
60
+ within(dashboardItem).getByText('A payment method is missing');
61
+ within(dashboardItem).getByText(
62
+ 'You must define a payment method to finalize your subscription.',
63
+ );
64
+ const link = within(dashboardItem).getByRole('link', { name: 'Define' });
65
+ expect(link).toHaveAttribute('href', `/courses/orders/${order.id}`);
66
+ await expectNoBannerError(
67
+ 'You have to define a payment method to finalize your subscription.',
68
+ );
69
+ });
70
+ });
71
+
72
+ describe('writable', () => {
73
+ it('renders elements to explain that a payment method is missing', async () => {
74
+ const order = CredentialOrderFactory({
75
+ state: OrderState.TO_SAVE_PAYMENT_METHOD,
76
+ }).one();
77
+
78
+ fetchMock.get(
79
+ 'https://joanie.endpoint/api/v1.0/orders/',
80
+ { results: [order], next: null, previous: null, count: null },
81
+ { overwriteRoutes: true },
82
+ );
83
+
84
+ const url = `begin:https://joanie.endpoint/api/v1.0/orders/?`;
85
+ fetchMock.get(url, [order]);
86
+
87
+ const { product } = mockCourseProductWithOrder(order);
88
+
89
+ render(
90
+ <DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
91
+ {
92
+ wrapper: BaseJoanieAppWrapper,
93
+ },
94
+ );
95
+
96
+ const dashboardItem = await screen.findByTestId(`dashboard-item-order-${order.id}`);
97
+ within(dashboardItem).getByRole('heading', { level: 5, name: product.title });
98
+ expect(
99
+ within(dashboardItem).queryByText('A payment method is missing'),
100
+ ).not.toBeInTheDocument();
101
+ expect(within(dashboardItem).queryByRole('link', { name: 'Define' })).not.toBeInTheDocument();
102
+ await expectBannerError('You have to define a payment method to finalize your subscription.');
103
+ const link = screen.getByRole('link', { name: 'define a payment method' });
104
+ expect(link).toHaveAttribute('href', '#dashboard-item-payment-method');
105
+
106
+ // The payment block should display information about the missing payment method
107
+ const paymentBlock = screen.getByTestId('dashboard-item-payment-method');
108
+ const title = within(paymentBlock).getByText('Payment');
109
+ expect(title.parentElement).toHaveClass('dashboard-splitted-card__item__title--dot');
110
+ within(paymentBlock).getByText(
111
+ 'To finalize your subscription, you must define a payment method.',
112
+ );
113
+ within(paymentBlock).getByRole('button', { name: 'Define' });
114
+ });
115
+ });
116
+ });
@@ -0,0 +1,175 @@
1
+ import { Alert, Button, useModal, VariantType } from '@openfun/cunningham-react';
2
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
+ import classNames from 'classnames';
4
+ import { CredentialOrder, CredentialProduct, OrderState } from 'types/Joanie';
5
+ import { OrderHelper } from 'utils/OrderHelper';
6
+ import { useCourseProduct } from 'hooks/useCourseProducts';
7
+ import { OrderPaymentDetailsModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal';
8
+ import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
9
+ import { SaleTunnel } from 'components/SaleTunnel';
10
+ import { Spinner } from 'components/Spinner';
11
+ import PaymentScheduleHelper from 'utils/PaymentScheduleHelper';
12
+
13
+ const messages = defineMessages({
14
+ paymentTitle: {
15
+ id: 'components.DashboardItemOrder.Installment.paymentTitle',
16
+ description: 'Label for the payment block',
17
+ defaultMessage: 'Payment',
18
+ },
19
+ paymentMethodMissingMessage: {
20
+ id: 'components.DashboardItemOrder.Installment.paymentMethodMissingMessage',
21
+ description: 'Message displayed when payment method is missing',
22
+ defaultMessage: 'To finalize your subscription, you must define a payment method.',
23
+ },
24
+ paymentInactiveDescription: {
25
+ id: 'components.DashboardItemOrder.Installment.paymentInactiveDescription',
26
+ description:
27
+ 'Explanation displayed when the order is not yet active and do not miss payment method',
28
+ defaultMessage:
29
+ 'You will able to manage your payment installment here once your subscription is finalized.',
30
+ },
31
+ paymentNeededMessage: {
32
+ id: 'components.DashboardItemOrder.Installment.paymentNeededMessage',
33
+ description: 'Message displayed when payment is needed',
34
+ defaultMessage: 'A payment failed, please update your payment method',
35
+ },
36
+ paymentNeededButton: {
37
+ id: 'components.DashboardItemOrder.Installment.paymentNeededButton',
38
+ description: 'Button label for the payment needed message',
39
+ defaultMessage: 'Pay {amount}',
40
+ },
41
+ paymentLabel: {
42
+ id: 'components.DashboardItemOrder.Installment.paymentLabel',
43
+ description: 'Label for the payment block',
44
+ defaultMessage: 'You can see and manage all installments.',
45
+ },
46
+ paymentButton: {
47
+ id: 'components.DashboardItemOrder.Installment.paymentButton',
48
+ description: 'Button label for the payment block',
49
+ defaultMessage: 'Manage payment',
50
+ },
51
+ defineButton: {
52
+ id: 'components.DashboardItemOrder.Installment.defineButton',
53
+ description: 'Button label to define payment method',
54
+ defaultMessage: 'Define',
55
+ },
56
+ });
57
+
58
+ type Props = {
59
+ order: CredentialOrder;
60
+ };
61
+
62
+ const Installment = ({ order }: Props) => {
63
+ const isActive = OrderHelper.isActive(order);
64
+ const failedInstallment = PaymentScheduleHelper.getFailedInstallment(order.payment_schedule);
65
+ const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
66
+ const shouldDisplayDot = needsPaymentMethod || !!failedInstallment;
67
+
68
+ return (
69
+ <div
70
+ id="dashboard-item-payment-method"
71
+ data-testid="dashboard-item-payment-method"
72
+ className="dashboard-splitted-card__item"
73
+ >
74
+ <div
75
+ className={classNames('dashboard-splitted-card__item__title', {
76
+ 'dashboard-splitted-card__item__title--dot': shouldDisplayDot,
77
+ })}
78
+ >
79
+ <span>
80
+ <FormattedMessage {...messages.paymentTitle} />
81
+ </span>
82
+ </div>
83
+ {!isActive && !needsPaymentMethod && (
84
+ <p className="dashboard-splitted-card__item__description">
85
+ <FormattedMessage {...messages.paymentInactiveDescription} />
86
+ </p>
87
+ )}
88
+ <PaymentMethodManager order={order} />
89
+ {isActive && <InstallmentManager order={order} />}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const PaymentMethodManager = ({ order }: Props) => {
95
+ const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
96
+ const { item: relation, states } = useCourseProduct({
97
+ course_id: order.course.code,
98
+ product_id: order.product_id,
99
+ });
100
+ const modal = useModal({ isOpenDefault: false });
101
+
102
+ if (!order || states.fetching) {
103
+ return <Spinner size="small" />;
104
+ }
105
+
106
+ return (
107
+ <>
108
+ {needsPaymentMethod && (
109
+ <div>
110
+ <p>
111
+ <FormattedMessage {...messages.paymentMethodMissingMessage} />
112
+ </p>
113
+ <Button size="small" onClick={modal.open}>
114
+ <FormattedMessage {...messages.defineButton} />
115
+ </Button>
116
+ </div>
117
+ )}
118
+ <SaleTunnel
119
+ {...modal}
120
+ product={relation.product as CredentialProduct}
121
+ course={relation.course}
122
+ />
123
+ </>
124
+ );
125
+ };
126
+
127
+ const InstallmentManager = ({ order }: Props) => {
128
+ const intl = useIntl();
129
+ const modal = useModal();
130
+ const retryModal = useModal();
131
+ const failedInstallment = PaymentScheduleHelper.getFailedInstallment(order.payment_schedule);
132
+
133
+ const pay = async () => {
134
+ retryModal.open();
135
+ };
136
+ return (
137
+ <>
138
+ {failedInstallment && (
139
+ <Alert
140
+ className="mb-t"
141
+ type={VariantType.ERROR}
142
+ buttons={
143
+ <Button size="small" onClick={pay}>
144
+ <FormattedMessage
145
+ {...messages.paymentNeededButton}
146
+ values={{
147
+ amount: intl.formatNumber(failedInstallment.amount, {
148
+ style: 'currency',
149
+ currency: failedInstallment.currency,
150
+ }),
151
+ }}
152
+ />
153
+ </Button>
154
+ }
155
+ >
156
+ <FormattedMessage {...messages.paymentNeededMessage} />
157
+ </Alert>
158
+ )}
159
+ <div className="dashboard-splitted-card__item__description">
160
+ <FormattedMessage {...messages.paymentLabel} />
161
+ </div>
162
+ <div className="dashboard-splitted-card__item__actions">
163
+ <Button size="small" color="secondary" onClick={modal.open}>
164
+ <FormattedMessage {...messages.paymentButton} />
165
+ </Button>
166
+ </div>
167
+ <OrderPaymentDetailsModal {...modal} order={order} />
168
+ {failedInstallment && (
169
+ <OrderPaymentRetryModal {...retryModal} installment={failedInstallment} order={order} />
170
+ )}
171
+ </>
172
+ );
173
+ };
174
+
175
+ export default Installment;
@@ -0,0 +1,7 @@
1
+ .order-payment-details {
2
+ &__title {
3
+ font-weight: var(--c--theme--font--weights--bold);
4
+ font-size: var(--c--theme--font--sizes--l);
5
+ text-align: left;
6
+ }
7
+ }
@@ -0,0 +1,127 @@
1
+ import {
2
+ Alert,
3
+ Button,
4
+ Loader,
5
+ Modal,
6
+ ModalProps,
7
+ ModalSize,
8
+ useModal,
9
+ VariantType,
10
+ } from '@openfun/cunningham-react';
11
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
12
+ import { useState, useEffect } from 'react';
13
+ import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
14
+ import { CreditCard, Order } from 'types/Joanie';
15
+ import { CreditCardSelector } from 'components/CreditCardSelector';
16
+ import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
17
+ import { Maybe } from 'types/utils';
18
+ import { useCreditCard } from 'hooks/useCreditCards';
19
+ import PaymentScheduleHelper from 'utils/PaymentScheduleHelper';
20
+
21
+ const messages = defineMessages({
22
+ title: {
23
+ id: 'components.DashboardItemOrder.PaymentModal.title',
24
+ defaultMessage: 'Payment details',
25
+ description: 'Title of the payment modal',
26
+ },
27
+ scheduleTitle: {
28
+ id: 'components.DashboardItemOrder.PaymentModal.scheduleTitle',
29
+ defaultMessage: 'Payment schedule',
30
+ description: 'Title of the payment schedule',
31
+ },
32
+ paymentMethodTitle: {
33
+ id: 'components.DashboardItemOrder.PaymentModal.paymentMethodTitle',
34
+ defaultMessage: 'Payment method',
35
+ description: 'Title of the payment method section',
36
+ },
37
+ paymentNeededMessage: {
38
+ id: 'components.DashboardItemOrder.paymentNeededMessage',
39
+ description: 'Message displayed when payment is needed',
40
+ defaultMessage: 'The payment failed, please update your payment method',
41
+ },
42
+ paymentNeededButton: {
43
+ id: 'components.DashboardItemOrder.paymentNeededButton',
44
+ description: 'Button label for the payment needed message',
45
+ defaultMessage: 'Pay {amount}',
46
+ },
47
+ });
48
+
49
+ interface PaymentModalProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
50
+ order: Order;
51
+ }
52
+
53
+ export const OrderPaymentDetailsModal = ({ order, ...props }: PaymentModalProps) => {
54
+ const intl = useIntl();
55
+ const retryModal = useModal();
56
+ const failedInstallment = PaymentScheduleHelper.getFailedInstallment(order.payment_schedule);
57
+
58
+ return (
59
+ <>
60
+ <Modal {...props} size={ModalSize.MEDIUM} title={intl.formatMessage(messages.title)}>
61
+ <h3 className="order-payment-details__title mb-s">
62
+ <FormattedMessage {...messages.paymentMethodTitle} />
63
+ </h3>
64
+ <CreditCardSelectorWrapper selectedCreditCardId={order.credit_card_id} />
65
+ <h3 className="order-payment-details__title mb-s mt-b">
66
+ <FormattedMessage {...messages.scheduleTitle} />
67
+ </h3>
68
+ {failedInstallment && (
69
+ <Alert
70
+ className="mb-t"
71
+ type={VariantType.ERROR}
72
+ buttons={
73
+ <Button size="small" onClick={retryModal.open}>
74
+ <FormattedMessage
75
+ {...messages.paymentNeededButton}
76
+ values={{
77
+ amount: intl.formatNumber(failedInstallment.amount, {
78
+ style: 'currency',
79
+ currency: failedInstallment.currency,
80
+ }),
81
+ }}
82
+ />
83
+ </Button>
84
+ }
85
+ >
86
+ <FormattedMessage {...messages.paymentNeededMessage} />
87
+ </Alert>
88
+ )}
89
+ {order.payment_schedule && <PaymentScheduleGrid schedule={order.payment_schedule} />}
90
+ </Modal>
91
+ {failedInstallment && (
92
+ <OrderPaymentRetryModal {...retryModal} installment={failedInstallment} order={order} />
93
+ )}
94
+ </>
95
+ );
96
+ };
97
+
98
+ const CreditCardSelectorWrapper = ({
99
+ selectedCreditCardId,
100
+ }: {
101
+ selectedCreditCardId: Maybe<string>;
102
+ }) => {
103
+ const {
104
+ item: creditCard,
105
+ states: { fetching },
106
+ } = useCreditCard(selectedCreditCardId);
107
+ const [selectedCreditCard, setSelectedCreditCard] = useState<Maybe<CreditCard>>(creditCard);
108
+
109
+ useEffect(() => {
110
+ if (!selectedCreditCard && creditCard) {
111
+ setSelectedCreditCard(creditCard);
112
+ }
113
+ }, [creditCard]);
114
+
115
+ if (fetching) {
116
+ return <Loader size="small" />;
117
+ }
118
+
119
+ return (
120
+ <CreditCardSelector
121
+ creditCard={selectedCreditCard || creditCard}
122
+ setCreditCard={setSelectedCreditCard}
123
+ quickRemove={false}
124
+ allowEdit={false}
125
+ />
126
+ );
127
+ };
@@ -0,0 +1,216 @@
1
+ import {
2
+ Alert,
3
+ Button,
4
+ Modal,
5
+ ModalProps,
6
+ ModalSize,
7
+ useModals,
8
+ VariantType,
9
+ } from '@openfun/cunningham-react';
10
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
11
+ import { useRef, useState } from 'react';
12
+ import {
13
+ CreditCard,
14
+ Order,
15
+ OrderEnrollment,
16
+ PaymentInstallment,
17
+ ACTIVE_ORDER_STATES,
18
+ } from 'types/Joanie';
19
+ import { CreditCardSelector } from 'components/CreditCardSelector';
20
+ import { useJoanieApi } from 'contexts/JoanieApiContext';
21
+ import { Payment, PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
22
+ import PaymentInterface from 'components/PaymentInterfaces';
23
+ import { PAYMENT_SETTINGS } from 'settings';
24
+ import { useOrders } from 'hooks/useOrders';
25
+ import { Spinner } from 'components/Spinner';
26
+
27
+ const messages = defineMessages({
28
+ title: {
29
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.title',
30
+ defaultMessage: 'Retry payment',
31
+ description: 'Title of the payment retry modal',
32
+ },
33
+ description: {
34
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.description',
35
+ defaultMessage:
36
+ 'The payment failed, please choose another payment method or add a new one during the payment',
37
+ description: 'Message displayed when payment is needed',
38
+ },
39
+ submit: {
40
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.submit',
41
+ defaultMessage: 'Pay {amount}',
42
+ description: 'Message displayed when payment is needed',
43
+ },
44
+ paymentInProgress: {
45
+ defaultMessage: 'Payment in progress',
46
+ description: 'Label for screen reader when a retry payment is in progress.',
47
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.paymentInProgress',
48
+ },
49
+ successTitle: {
50
+ defaultMessage: 'Payment successful',
51
+ description: 'Title of the payment success modal',
52
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.successTitle',
53
+ },
54
+ successDescription: {
55
+ defaultMessage: 'The payment was successful',
56
+ description: 'Description of the payment success modal',
57
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.successDescription',
58
+ },
59
+ errorFailedSubmitInstallmentPayment: {
60
+ defaultMessage: 'Failed to submit installment payment, please retry later.',
61
+ description: 'Error message when submitting installment payment fails',
62
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.errorFailedSubmitInstallmentPayment',
63
+ },
64
+ errorAbortingPolling: {
65
+ defaultMessage:
66
+ 'Your payment has succeeded but your order validation is taking too long, you can close this dialog and come back later.',
67
+ description: 'Error message when submitting installment payment fails',
68
+ id: 'components.DashboardItemOrder.OrderPaymentRetryModal.errorAbortingPolling',
69
+ },
70
+ });
71
+
72
+ interface Props extends Pick<ModalProps, 'isOpen' | 'onClose'> {
73
+ installment: PaymentInstallment;
74
+ order: Order | OrderEnrollment;
75
+ }
76
+
77
+ enum ComponentStates {
78
+ IDLE = 'idle',
79
+ LOADING = 'loading',
80
+ ERROR = 'error',
81
+ }
82
+
83
+ export const OrderPaymentRetryModal = ({ installment, order, ...props }: Props) => {
84
+ const intl = useIntl();
85
+ const API = useJoanieApi();
86
+ const timeoutRef = useRef<NodeJS.Timeout>();
87
+ const { methods: orderMethods } = useOrders(undefined, { enabled: false });
88
+ const [payment, setPayment] = useState<Payment>();
89
+ const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
90
+ const [error, setError] = useState<string>();
91
+ const [creditCard, setCreditCard] = useState<CreditCard>();
92
+ const modals = useModals();
93
+
94
+ const pay = async () => {
95
+ setState(ComponentStates.LOADING);
96
+ try {
97
+ const paymentResponse = await API.user.orders.submit_installment_payment(order.id, {
98
+ credit_card_id: creditCard?.id,
99
+ });
100
+ if (paymentResponse) {
101
+ setPayment(paymentResponse);
102
+ } else {
103
+ // In case of bug.
104
+ setError(intl.formatMessage(messages.errorFailedSubmitInstallmentPayment));
105
+ setState(ComponentStates.ERROR);
106
+ }
107
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
108
+ } catch (_error) {
109
+ setError(intl.formatMessage(messages.errorFailedSubmitInstallmentPayment));
110
+ setState(ComponentStates.ERROR);
111
+ }
112
+ };
113
+
114
+ const handleError = (messageId: string = PaymentErrorMessageId.ERROR_DEFAULT) => {
115
+ setState(ComponentStates.ERROR);
116
+ setError(messageId);
117
+ };
118
+
119
+ const isOrderValidated = async (id: string): Promise<Boolean> => {
120
+ const orderToCheck = await API.user.orders.get({ id });
121
+ return orderToCheck !== null && ACTIVE_ORDER_STATES.includes(orderToCheck.state);
122
+ };
123
+
124
+ const settled = async () => {
125
+ await orderMethods.invalidate();
126
+ props.onClose();
127
+ await modals.messageModal({
128
+ messageType: VariantType.SUCCESS,
129
+ title: intl.formatMessage(messages.successTitle),
130
+ children: intl.formatMessage(messages.successDescription),
131
+ });
132
+ };
133
+
134
+ const handleSuccess = () => {
135
+ let round = 0;
136
+
137
+ const checkOrderValidity = async () => {
138
+ if (round >= PAYMENT_SETTINGS.pollLimit) {
139
+ timeoutRef.current = undefined;
140
+ setState(ComponentStates.ERROR);
141
+ setError(intl.formatMessage(messages.errorAbortingPolling));
142
+ } else {
143
+ const isValidated = await isOrderValidated(order.id);
144
+ if (isValidated) {
145
+ setState(ComponentStates.IDLE);
146
+ timeoutRef.current = undefined;
147
+ settled();
148
+ } else {
149
+ round++;
150
+ timeoutRef.current = setTimeout(checkOrderValidity, PAYMENT_SETTINGS.pollInterval);
151
+ }
152
+ }
153
+ };
154
+
155
+ checkOrderValidity();
156
+ };
157
+
158
+ return (
159
+ <>
160
+ <Modal
161
+ {...props}
162
+ size={ModalSize.MEDIUM}
163
+ title={intl.formatMessage(messages.title)}
164
+ closeOnEsc={state !== ComponentStates.LOADING}
165
+ preventClose={state === ComponentStates.LOADING}
166
+ hideCloseButton={state === ComponentStates.LOADING}
167
+ actions={
168
+ <Button
169
+ color="primary"
170
+ size="small"
171
+ fullWidth={true}
172
+ onClick={pay}
173
+ disabled={state === ComponentStates.LOADING}
174
+ data-testid="order-payment-retry-modal-submit-button"
175
+ >
176
+ {state === ComponentStates.LOADING ? (
177
+ <Spinner theme="light" aria-labelledby="payment-in-progress">
178
+ <span id="payment-in-progress">
179
+ <FormattedMessage {...messages.paymentInProgress} />
180
+ </span>
181
+ </Spinner>
182
+ ) : (
183
+ <FormattedMessage
184
+ {...messages.submit}
185
+ values={{
186
+ amount: intl.formatNumber(installment.amount, {
187
+ style: 'currency',
188
+ currency: installment.currency,
189
+ }),
190
+ }}
191
+ />
192
+ )}
193
+ </Button>
194
+ }
195
+ >
196
+ {error && (
197
+ <Alert type={VariantType.ERROR} className="mb-t">
198
+ {error}
199
+ </Alert>
200
+ )}
201
+ <Alert className="mb-b">
202
+ <FormattedMessage {...messages.description} />
203
+ </Alert>
204
+ <CreditCardSelector
205
+ creditCard={creditCard}
206
+ setCreditCard={setCreditCard}
207
+ quickRemove={state !== ComponentStates.LOADING}
208
+ allowEdit={state !== ComponentStates.LOADING}
209
+ />
210
+ </Modal>
211
+ {state === ComponentStates.LOADING && payment && (
212
+ <PaymentInterface {...payment} onError={handleError} onSuccess={handleSuccess} />
213
+ )}
214
+ </>
215
+ );
216
+ };