richie-education 2.29.1-dev32 → 2.29.1-dev37

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 (21) hide show
  1. package/js/components/ContractStatus/index.spec.tsx +1 -1
  2. package/js/components/ContractStatus/index.tsx +1 -1
  3. package/js/components/PurchaseButton/index.tsx +9 -27
  4. package/js/components/SaleTunnel/index.tsx +2 -1
  5. package/js/pages/DashboardOrderLayout/index.spec.tsx +10 -1
  6. package/js/utils/ProductHelper/index.spec.ts +322 -166
  7. package/js/utils/ProductHelper/index.ts +32 -0
  8. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +2 -2
  9. package/js/widgets/Dashboard/components/DashboardItem/Order/CertificateItem/index.tsx +51 -0
  10. package/js/widgets/Dashboard/components/DashboardItem/Order/ContractItem/index.tsx +52 -0
  11. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemNotResumable.spec.tsx +109 -0
  12. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +105 -0
  13. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +103 -324
  14. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +59 -8
  15. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +12 -1
  16. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemSavePaymentMethod.spec.tsx +116 -0
  17. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +174 -0
  18. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +2 -1
  19. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +150 -0
  20. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +29 -3
  21. package/package.json +2 -2
@@ -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,174 @@
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
+
12
+ const messages = defineMessages({
13
+ paymentTitle: {
14
+ id: 'components.DashboardItemOrder.Installment.paymentTitle',
15
+ description: 'Label for the payment block',
16
+ defaultMessage: 'Payment',
17
+ },
18
+ paymentMethodMissingMessage: {
19
+ id: 'components.DashboardItemOrder.Installment.paymentMethodMissingMessage',
20
+ description: 'Message displayed when payment method is missing',
21
+ defaultMessage: 'To finalize your subscription, you must define a payment method.',
22
+ },
23
+ paymentInactiveDescription: {
24
+ id: 'components.DashboardItemOrder.Installment.paymentInactiveDescription',
25
+ description:
26
+ 'Explanation displayed when the order is not yet active and do not miss payment method',
27
+ defaultMessage:
28
+ 'You will able to manage your payment installment here once your subscription is finalized.',
29
+ },
30
+ paymentNeededMessage: {
31
+ id: 'components.DashboardItemOrder.Installment.paymentNeededMessage',
32
+ description: 'Message displayed when payment is needed',
33
+ defaultMessage: 'A payment failed, please update your payment method',
34
+ },
35
+ paymentNeededButton: {
36
+ id: 'components.DashboardItemOrder.Installment.paymentNeededButton',
37
+ description: 'Button label for the payment needed message',
38
+ defaultMessage: 'Pay {amount}',
39
+ },
40
+ paymentLabel: {
41
+ id: 'components.DashboardItemOrder.Installment.paymentLabel',
42
+ description: 'Label for the payment block',
43
+ defaultMessage: 'You can see and manage all installments.',
44
+ },
45
+ paymentButton: {
46
+ id: 'components.DashboardItemOrder.Installment.paymentButton',
47
+ description: 'Button label for the payment block',
48
+ defaultMessage: 'Manage payment',
49
+ },
50
+ defineButton: {
51
+ id: 'components.DashboardItemOrder.Installment.defineButton',
52
+ description: 'Button label to define payment method',
53
+ defaultMessage: 'Define',
54
+ },
55
+ });
56
+
57
+ type Props = {
58
+ order: CredentialOrder;
59
+ };
60
+
61
+ const Installment = ({ order }: Props) => {
62
+ const isActive = OrderHelper.isActive(order);
63
+ const failedInstallment = OrderHelper.getFailedInstallment(order);
64
+ const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
65
+ const shouldDisplayDot = needsPaymentMethod || !!failedInstallment;
66
+
67
+ return (
68
+ <div
69
+ id="dashboard-item-payment-method"
70
+ data-testid="dashboard-item-payment-method"
71
+ className="dashboard-splitted-card__item"
72
+ >
73
+ <div
74
+ className={classNames('dashboard-splitted-card__item__title', {
75
+ 'dashboard-splitted-card__item__title--dot': shouldDisplayDot,
76
+ })}
77
+ >
78
+ <span>
79
+ <FormattedMessage {...messages.paymentTitle} />
80
+ </span>
81
+ </div>
82
+ {!isActive && !needsPaymentMethod && (
83
+ <p className="dashboard-splitted-card__item__description">
84
+ <FormattedMessage {...messages.paymentInactiveDescription} />
85
+ </p>
86
+ )}
87
+ <PaymentMethodManager order={order} />
88
+ {isActive && <InstallmentManager order={order} />}
89
+ </div>
90
+ );
91
+ };
92
+
93
+ const PaymentMethodManager = ({ order }: Props) => {
94
+ const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
95
+ const { item: relation, states } = useCourseProduct({
96
+ course_id: order.course.code,
97
+ product_id: order.product_id,
98
+ });
99
+ const modal = useModal({ isOpenDefault: false });
100
+
101
+ if (!order || states.fetching) {
102
+ return <Spinner size="small" />;
103
+ }
104
+
105
+ return (
106
+ <>
107
+ {needsPaymentMethod && (
108
+ <div>
109
+ <p>
110
+ <FormattedMessage {...messages.paymentMethodMissingMessage} />
111
+ </p>
112
+ <Button size="small" onClick={modal.open}>
113
+ <FormattedMessage {...messages.defineButton} />
114
+ </Button>
115
+ </div>
116
+ )}
117
+ <SaleTunnel
118
+ {...modal}
119
+ product={relation.product as CredentialProduct}
120
+ course={relation.course}
121
+ />
122
+ </>
123
+ );
124
+ };
125
+
126
+ const InstallmentManager = ({ order }: Props) => {
127
+ const intl = useIntl();
128
+ const modal = useModal();
129
+ const retryModal = useModal();
130
+ const failedInstallment = OrderHelper.getFailedInstallment(order);
131
+
132
+ const pay = async () => {
133
+ retryModal.open();
134
+ };
135
+ return (
136
+ <>
137
+ {failedInstallment && (
138
+ <Alert
139
+ className="mb-t"
140
+ type={VariantType.ERROR}
141
+ buttons={
142
+ <Button size="small" onClick={pay}>
143
+ <FormattedMessage
144
+ {...messages.paymentNeededButton}
145
+ values={{
146
+ amount: intl.formatNumber(failedInstallment.amount, {
147
+ style: 'currency',
148
+ currency: failedInstallment.currency,
149
+ }),
150
+ }}
151
+ />
152
+ </Button>
153
+ }
154
+ >
155
+ <FormattedMessage {...messages.paymentNeededMessage} />
156
+ </Alert>
157
+ )}
158
+ <div className="dashboard-splitted-card__item__description">
159
+ <FormattedMessage {...messages.paymentLabel} />
160
+ </div>
161
+ <div className="dashboard-splitted-card__item__actions">
162
+ <Button size="small" color="secondary" onClick={modal.open}>
163
+ <FormattedMessage {...messages.paymentButton} />
164
+ </Button>
165
+ </div>
166
+ <OrderPaymentDetailsModal {...modal} order={order} />
167
+ {failedInstallment && (
168
+ <OrderPaymentRetryModal {...retryModal} installment={failedInstallment} order={order} />
169
+ )}
170
+ </>
171
+ );
172
+ };
173
+
174
+ export default Installment;
@@ -26,7 +26,7 @@ const messages = defineMessages({
26
26
  },
27
27
  scheduleTitle: {
28
28
  id: 'components.DashboardItemOrder.PaymentModal.scheduleTitle',
29
- defaultMessage: 'Repayment schedule',
29
+ defaultMessage: 'Payment schedule',
30
30
  description: 'Title of the payment schedule',
31
31
  },
32
32
  paymentMethodTitle: {
@@ -54,6 +54,7 @@ export const OrderPaymentDetailsModal = ({ order, ...props }: PaymentModalProps)
54
54
  const intl = useIntl();
55
55
  const retryModal = useModal();
56
56
  const failedInstallment = OrderHelper.getFailedInstallment(order);
57
+
57
58
  return (
58
59
  <>
59
60
  <Modal {...props} size={ModalSize.MEDIUM} title={intl.formatMessage(messages.title)}>
@@ -0,0 +1,150 @@
1
+ import { defineMessages, FormattedMessage } from 'react-intl';
2
+ import { Button } from '@openfun/cunningham-react';
3
+ import { CredentialOrder, Product } from 'types/Joanie';
4
+ import { AddressView } from 'components/Address';
5
+ import ContractItem from '../ContractItem';
6
+ import Installment from '../Installment';
7
+
8
+ const messages = defineMessages({
9
+ contactDescription: {
10
+ id: 'components.DashboardItemOrder.OrganizationBlock.contactDescription',
11
+ description: 'Description of the contact information for the organization',
12
+ defaultMessage: 'Your training reference is {name} - {email}.',
13
+ },
14
+ contactButton: {
15
+ id: 'components.DashboardItemOrder.OrganizationBlock.contactButton',
16
+ description: 'Button to contact the organization',
17
+ defaultMessage: 'Contact',
18
+ },
19
+ organizationHeader: {
20
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationHeader',
21
+ description: 'Header of the organization section',
22
+ defaultMessage: 'This training is provided by',
23
+ },
24
+ organizationLogoAlt: {
25
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationLogoAlt',
26
+ description: 'Alt text for the organization logo',
27
+ defaultMessage: 'Logo of the organization',
28
+ },
29
+ organizationMailContactLabel: {
30
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationMailContactLabel',
31
+ description: 'Label for the organization mail contact',
32
+ defaultMessage: 'Email',
33
+ },
34
+ organizationPhoneContactLabel: {
35
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationPhoneContactLabel',
36
+ description: 'Label for the organization phone contact',
37
+ defaultMessage: 'Phone',
38
+ },
39
+ organizationDpoContactLabel: {
40
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationDpoContactLabel',
41
+ description: 'Label for the organization DPO contact',
42
+ defaultMessage: 'Data protection email',
43
+ },
44
+ organizationSubtitleAddress: {
45
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationSubtitleAddress',
46
+ description: 'Subtitle for the organization address section',
47
+ defaultMessage: 'Address',
48
+ },
49
+ organizationSubtitleContacts: {
50
+ id: 'components.DashboardItemOrder.OrganizationBlock.organizationSubtitleContacts',
51
+ description: 'Subtitle for the organization contacts section',
52
+ defaultMessage: 'Contacts',
53
+ },
54
+ });
55
+
56
+ type Props = {
57
+ product: Product;
58
+ order: CredentialOrder;
59
+ };
60
+
61
+ const OrganizationBlock = ({ order, product }: Props) => {
62
+ const { organization } = order;
63
+ if (!organization) {
64
+ return null;
65
+ }
66
+
67
+ const showContactsBlock =
68
+ organization.contact_email || organization.contact_phone || organization.dpo_email;
69
+
70
+ return (
71
+ <div className="dashboard-splitted-card mt-s" data-testid="organization-block">
72
+ <div className="dashboard-splitted-card__column order-organization__caption">
73
+ <div className="dashboard-item-order__organization">
74
+ <div className="dashboard-item-order__organization__header">
75
+ <FormattedMessage {...messages.organizationHeader} />
76
+ </div>
77
+ <div
78
+ className="dashboard-item-order__organization__logo"
79
+ style={{
80
+ backgroundImage: `url(${organization.logo?.src})`,
81
+ }}
82
+ />
83
+ <div className="dashboard-item-order__organization__name">{organization.title}</div>
84
+ </div>
85
+ </div>
86
+ <div className="dashboard-splitted-card__separator order-organization__separator" />
87
+ <div className="dashboard-splitted-card__column order-organization__items">
88
+ <ContractItem order={order} product={product} />
89
+ {showContactsBlock && (
90
+ <div className="dashboard-splitted-card__item">
91
+ <div className="dashboard-splitted-card__item__title">
92
+ <FormattedMessage {...messages.organizationSubtitleContacts} />
93
+ </div>
94
+ <div className="dashboard-splitted-card__item__description">
95
+ {organization.contact_email && (
96
+ <div className="organization-block__contact__item">
97
+ <FormattedMessage {...messages.organizationMailContactLabel} />
98
+ <Button
99
+ size="small"
100
+ color="tertiary"
101
+ href={'mailto:' + (organization.contact_email ?? '')}
102
+ >
103
+ {organization.contact_email}
104
+ </Button>
105
+ </div>
106
+ )}
107
+ {organization.contact_phone && (
108
+ <div className="organization-block__contact__item">
109
+ <FormattedMessage {...messages.organizationPhoneContactLabel} />
110
+ <Button
111
+ size="small"
112
+ color="tertiary"
113
+ href={'tel:' + (organization.contact_phone ?? '')}
114
+ >
115
+ {organization.contact_phone}
116
+ </Button>
117
+ </div>
118
+ )}
119
+ {organization.dpo_email && (
120
+ <div className="organization-block__contact__item">
121
+ <FormattedMessage {...messages.organizationDpoContactLabel} />
122
+ <Button
123
+ size="small"
124
+ color="tertiary"
125
+ href={'mailto:' + (organization.dpo_email ?? '')}
126
+ >
127
+ {organization.dpo_email}
128
+ </Button>
129
+ </div>
130
+ )}
131
+ </div>
132
+ </div>
133
+ )}
134
+ {organization.address && (
135
+ <div className="dashboard-splitted-card__item dashboard-splitted-card__item__address">
136
+ <div className="dashboard-splitted-card__item__title">
137
+ <FormattedMessage {...messages.organizationSubtitleAddress} />
138
+ </div>
139
+ <div className="dashboard-splitted-card__item__description">
140
+ <AddressView address={organization.address} />
141
+ </div>
142
+ </div>
143
+ )}
144
+ <Installment order={order} />
145
+ </div>
146
+ </div>
147
+ );
148
+ };
149
+
150
+ export default OrganizationBlock;
@@ -7,6 +7,7 @@ import Banner, { BannerType } from 'components/Banner';
7
7
  import { isCredentialOrder } from 'pages/DashboardCourses/useOrdersEnrollments';
8
8
  import { handle } from 'utils/errors/handle';
9
9
  import { OrderHelper } from 'utils/OrderHelper';
10
+ import { OrderState } from 'types/Joanie';
10
11
  import { DashboardItemOrder } from '../DashboardItem/Order/DashboardItemOrder';
11
12
 
12
13
  const messages = defineMessages({
@@ -16,12 +17,12 @@ const messages = defineMessages({
16
17
  id: 'components.DashboardOrderLoader.loading',
17
18
  },
18
19
  signatureNeeded: {
19
- defaultMessage: 'You need to {signLink} before enrolling in a course run',
20
+ defaultMessage: 'You have to {signLink} to finalize your subscription.',
20
21
  description: 'Banner displayed when the contract is not signed',
21
22
  id: 'components.DashboardOrderLoader.signatureNeeded',
22
23
  },
23
24
  signLink: {
24
- defaultMessage: 'sign your contract',
25
+ defaultMessage: 'sign your training contract',
25
26
  description: 'Link to sign the contract',
26
27
  id: 'components.DashboardOrderLoader.signLink',
27
28
  },
@@ -30,6 +31,16 @@ const messages = defineMessages({
30
31
  description: "Error message displayed when order's linked product type is not handle.",
31
32
  id: 'components.DashboardOrderLoader.wrongLinkedProductError',
32
33
  },
34
+ paymentMethodMissing: {
35
+ defaultMessage: 'You have to {definePaymentMethodLink} to finalize your subscription.',
36
+ description: 'Error message displayed when no payment method is defined.',
37
+ id: 'components.DashboardOrderLoader.paymentMethodMissing',
38
+ },
39
+ definePaymentMethodLink: {
40
+ defaultMessage: 'define a payment method',
41
+ description: 'Link to define a payment method',
42
+ id: 'components.DashboardOrderLoader.definePaymentMethodLink',
43
+ },
33
44
  });
34
45
 
35
46
  export const DashboardOrderLoader = () => {
@@ -50,6 +61,7 @@ export const DashboardOrderLoader = () => {
50
61
  const error = errorOrder || wrongLinkedProductError;
51
62
  const fetching = fetchingOrder;
52
63
  const needsSignature = order ? OrderHelper.orderNeedsSignature(order) : false;
64
+ const needsPaymentMethod = order?.state === OrderState.TO_SAVE_PAYMENT_METHOD;
53
65
 
54
66
  return (
55
67
  <>
@@ -67,7 +79,7 @@ export const DashboardOrderLoader = () => {
67
79
  message={
68
80
  intl.formatMessage(messages.signatureNeeded, {
69
81
  signLink: (
70
- <a href={'#dashboard-item-contract-' + order.id}>
82
+ <a href="#dashboard-item-contract">
71
83
  <FormattedMessage {...messages.signLink} />
72
84
  </a>
73
85
  ),
@@ -76,6 +88,20 @@ export const DashboardOrderLoader = () => {
76
88
  type={BannerType.ERROR}
77
89
  />
78
90
  )}
91
+ {order && needsPaymentMethod && (
92
+ <Banner
93
+ message={
94
+ intl.formatMessage(messages.paymentMethodMissing, {
95
+ definePaymentMethodLink: (
96
+ <a href="#dashboard-item-payment-method">
97
+ <FormattedMessage {...messages.definePaymentMethodLink} />
98
+ </a>
99
+ ),
100
+ }) as any
101
+ }
102
+ type={BannerType.ERROR}
103
+ />
104
+ )}
79
105
  </div>
80
106
  {credentialOrder && (
81
107
  <DashboardItemOrder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.29.1-dev32",
3
+ "version": "2.29.1-dev37",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -49,7 +49,7 @@
49
49
  "@formatjs/intl-relativetimeformat": "11.2.14",
50
50
  "@hookform/resolvers": "3.9.0",
51
51
  "@lyracom/embedded-form-glue": "1.4.2",
52
- "@openfun/cunningham-react": "2.9.3",
52
+ "@openfun/cunningham-react": "2.9.4",
53
53
  "@openfun/cunningham-tokens": "2.1.1",
54
54
  "@sentry/browser": "8.26.0",
55
55
  "@sentry/types": "8.26.0",