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,229 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { act, screen } from '@testing-library/react';
3
+ import fetchMock from 'fetch-mock';
4
+ import { userEvent } from '@testing-library/user-event';
5
+ import {
6
+ RichieContextFactory as mockRichieContextFactory,
7
+ UserFactory,
8
+ } from 'utils/test/factories/richie';
9
+ import { render } from 'utils/test/render';
10
+ import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
11
+ import OpenEdxFullNameForm from 'components/OpenEdxFullNameForm/index';
12
+ import { SaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
13
+ import { SaleTunnelContextFactory } from 'utils/test/factories/joanie';
14
+ import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
15
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
16
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
17
+ import { AppWrapperProps } from 'utils/test/wrappers/types';
18
+ import { expectBannerError } from 'utils/test/expectBanner';
19
+
20
+ jest.mock('utils/context', () => ({
21
+ __esModule: true,
22
+ default: mockRichieContextFactory({
23
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
24
+ joanie_backend: { endpoint: 'https://joanie.endpoint' },
25
+ }).one(),
26
+ }));
27
+
28
+ jest.mock('utils/errors/handle', () => ({
29
+ handle: jest.fn(),
30
+ }));
31
+
32
+ describe('OpenEdxFullNameForm', () => {
33
+ let submitCallbacks: Record<PropertyKey, () => Promise<void>> = {};
34
+ const Wrapper = ({ children, ...props }: PropsWithChildren<AppWrapperProps>) => (
35
+ <BaseJoanieAppWrapper {...props}>
36
+ <SaleTunnelContext.Provider
37
+ value={SaleTunnelContextFactory({
38
+ registerSubmitCallback: (key: string, callback: () => Promise<void>) => {
39
+ submitCallbacks[key] = callback;
40
+ },
41
+ }).one()}
42
+ >
43
+ {children}
44
+ </SaleTunnelContext.Provider>
45
+ ,
46
+ </BaseJoanieAppWrapper>
47
+ );
48
+
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ fetchMock.restore();
52
+ submitCallbacks = {};
53
+ });
54
+ setupJoanieSession();
55
+
56
+ it('should not populate form with username', async () => {
57
+ const user = UserFactory({ full_name: '' }).one();
58
+ const queryClient = createTestQueryClient({ user });
59
+ const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
60
+ name: user.full_name,
61
+ username: user.username,
62
+ email: user.email,
63
+ }).one();
64
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
65
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
66
+ 'pref-lang': prefLang,
67
+ });
68
+
69
+ render(<OpenEdxFullNameForm />, {
70
+ queryOptions: { client: queryClient },
71
+ wrapper: Wrapper,
72
+ });
73
+
74
+ const $input = await screen.findByRole('textbox', { name: 'Full name' });
75
+ expect($input).toHaveValue('');
76
+ });
77
+
78
+ it('should populate form with existing full name', async () => {
79
+ const user = UserFactory().one();
80
+ const queryClient = createTestQueryClient({ user });
81
+ const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
82
+ name: user.full_name,
83
+ username: user.username,
84
+ email: user.email,
85
+ }).one();
86
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
87
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
88
+ 'pref-lang': prefLang,
89
+ });
90
+
91
+ render(<OpenEdxFullNameForm />, {
92
+ queryOptions: { client: queryClient },
93
+ wrapper: Wrapper,
94
+ });
95
+
96
+ const $input = await screen.findByRole('textbox', { name: 'Full name' });
97
+ expect($input).toHaveValue(user.full_name);
98
+ });
99
+
100
+ it('should require a value to submit the form', async () => {
101
+ const user = UserFactory({ full_name: '' }).one();
102
+ const queryClient = createTestQueryClient({ user });
103
+ const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
104
+ name: user.full_name,
105
+ username: user.username,
106
+ email: user.email,
107
+ }).one();
108
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
109
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
110
+ 'pref-lang': prefLang,
111
+ });
112
+
113
+ expect(submitCallbacks).toEqual({});
114
+
115
+ render(<OpenEdxFullNameForm />, {
116
+ queryOptions: { client: queryClient },
117
+ wrapper: Wrapper,
118
+ });
119
+
120
+ expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
121
+
122
+ const $input = await screen.findByRole('textbox', { name: 'Full name' });
123
+ expect($input).toHaveValue('');
124
+
125
+ // Submit the form
126
+ await act(async () => {
127
+ await expect(submitCallbacks.openEdxFullNameForm()).rejects.not.toBeUndefined();
128
+ });
129
+
130
+ screen.getByText('This field is required.');
131
+ });
132
+
133
+ it('should require a value with at least 3 chars to submit the form', async () => {
134
+ const user = UserFactory({ full_name: '' }).one();
135
+ const queryClient = createTestQueryClient({ user });
136
+ const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
137
+ name: user.full_name,
138
+ username: user.username,
139
+ email: user.email,
140
+ }).one();
141
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile);
142
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
143
+ 'pref-lang': prefLang,
144
+ });
145
+
146
+ expect(submitCallbacks).toEqual({});
147
+
148
+ render(<OpenEdxFullNameForm />, {
149
+ queryOptions: { client: queryClient },
150
+ wrapper: Wrapper,
151
+ });
152
+
153
+ expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
154
+
155
+ const $input = await screen.findByRole('textbox', { name: 'Full name' });
156
+ expect($input).toHaveValue('');
157
+
158
+ const eventHandler = userEvent.setup();
159
+ await eventHandler.type($input, 'Jo');
160
+
161
+ // Submit the form
162
+ await act(async () => {
163
+ await expect(submitCallbacks.openEdxFullNameForm()).rejects.not.toBeUndefined();
164
+ });
165
+
166
+ screen.getByText('The minimum length is 3 chars.');
167
+ });
168
+
169
+ it('should submit the form', async () => {
170
+ const user = UserFactory({ full_name: '' }).one();
171
+ const queryClient = createTestQueryClient({ user });
172
+ const { 'pref-lang': prefLang, ...profile } = OpenEdxApiProfileFactory({
173
+ name: user.full_name,
174
+ username: user.username,
175
+ email: user.email,
176
+ }).one();
177
+ fetchMock
178
+ .get(`https://auth.test/api/user/v1/accounts/${user.username}`, profile)
179
+ .get(`https://auth.test/api/user/v1/preferences/${user.username}`, {
180
+ 'pref-lang': prefLang,
181
+ });
182
+
183
+ expect(submitCallbacks).toEqual({});
184
+
185
+ render(<OpenEdxFullNameForm />, {
186
+ queryOptions: { client: queryClient },
187
+ wrapper: Wrapper,
188
+ });
189
+
190
+ expect(submitCallbacks.hasOwnProperty('openEdxFullNameForm')).toBe(true);
191
+
192
+ const $input = await screen.findByRole('textbox', { name: 'Full name' });
193
+ expect($input).toHaveValue('');
194
+
195
+ const eventHandler = userEvent.setup();
196
+ await eventHandler.type($input, 'John Doe');
197
+
198
+ // Submit the form
199
+ fetchMock
200
+ .patch(`https://auth.test/api/user/v1/accounts/${user.username}`, 200)
201
+ .patch(`https://auth.test/api/user/v1/preferences/${user.username}`, 200)
202
+ .get('https://auth.test/api/v1.0/user/me', { ...user, full_name: 'John Doe' })
203
+ .get(
204
+ `https://auth.test/api/user/v1/accounts/JohnDoe`,
205
+ { ...profile, name: 'John Doe' },
206
+ { overwriteRoutes: true },
207
+ );
208
+
209
+ await act(async () => {
210
+ await submitCallbacks.openEdxFullNameForm();
211
+ });
212
+
213
+ expect($input).toHaveValue('John Doe');
214
+ });
215
+
216
+ it('should display error if request to retrieve account fails', async () => {
217
+ const user = UserFactory().one();
218
+ const queryClient = createTestQueryClient({ user });
219
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${user.username}`, 500);
220
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${user.username}`, 500);
221
+
222
+ render(<OpenEdxFullNameForm />, {
223
+ queryOptions: { client: queryClient },
224
+ wrapper: Wrapper,
225
+ });
226
+
227
+ await expectBannerError('An error occurred while fetching your profile. Please retry later.');
228
+ });
229
+ });
@@ -9,7 +9,6 @@ import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
9
9
  import Form, { getLocalizedCunninghamErrorProp } from 'components/Form';
10
10
  import { Spinner } from 'components/Spinner';
11
11
  import Banner, { BannerType } from 'components/Banner';
12
- import { UserHelper } from 'utils/UserHelper';
13
12
  import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
14
13
 
15
14
  const messages = defineMessages({
@@ -51,7 +50,7 @@ export interface OpenEdxFullNameFormValues {
51
50
  }
52
51
 
53
52
  const validationSchema = Yup.object().shape({
54
- name: Yup.string().required(),
53
+ name: Yup.string().required().min(3),
55
54
  });
56
55
 
57
56
  const OpenEdxFullNameForm = () => {
@@ -70,7 +69,7 @@ const OpenEdxFullNameForm = () => {
70
69
 
71
70
  const defaultValues = useMemo(
72
71
  () => ({
73
- name: (openEdxProfileData ? UserHelper.getName(openEdxProfileData) : '')?.trim(),
72
+ name: (openEdxProfileData?.name || '')?.trim(),
74
73
  }),
75
74
  [openEdxProfileData],
76
75
  );
@@ -82,19 +81,20 @@ const OpenEdxFullNameForm = () => {
82
81
  resolver: yupResolver(validationSchema),
83
82
  });
84
83
 
85
- const { register, handleSubmit, reset, formState } = form;
84
+ const { getValues, register, handleSubmit, reset, formState } = form;
86
85
 
87
86
  useEffect(() => {
88
87
  if (openEdxProfileData) {
89
- reset({ name: (openEdxProfileData ? UserHelper.getName(openEdxProfileData) : '')?.trim() });
88
+ reset({ name: (openEdxProfileData?.name || '')?.trim() });
90
89
  }
91
90
  }, [openEdxProfileData]);
92
91
 
93
92
  useEffect(() => {
94
93
  registerSubmitCallback('openEdxFullNameForm', async () => {
95
94
  return new Promise<void>((resolve, reject) => {
95
+ const { name } = getValues();
96
96
  // Don't save if the form has not been modified.
97
- if (!formState.isDirty) {
97
+ if (name && !formState.isDirty) {
98
98
  resolve();
99
99
  return;
100
100
  }
@@ -110,7 +110,7 @@ const OpenEdxFullNameForm = () => {
110
110
  },
111
111
  onError: (e) => reject(e),
112
112
  });
113
- })();
113
+ }, reject)();
114
114
  });
115
115
  });
116
116
  return () => {
@@ -29,7 +29,7 @@ const LyraPopIn = ({
29
29
  if (error && typeof error === 'string') handle(`[LyraPopIn] - ${error}`);
30
30
 
31
31
  if (shouldAbort.current) {
32
- onError(PaymentErrorMessageId.ERROR_ABORTING);
32
+ onError(PaymentErrorMessageId.ERROR_ABORT);
33
33
  } else if (typeof error === 'string') {
34
34
  onError(error);
35
35
  } else {
@@ -109,7 +109,7 @@ const LyraPopIn = ({
109
109
 
110
110
  const handleClosePopIn = () => {
111
111
  if (shouldAbort.current === true) {
112
- onError(PaymentErrorMessageId.ERROR_ABORTING);
112
+ onError(PaymentErrorMessageId.ERROR_ABORT);
113
113
  }
114
114
  };
115
115
 
@@ -27,7 +27,7 @@ const PayplugLightbox = ({
27
27
  switch (event.data) {
28
28
  case 'closePayPlugFrame':
29
29
  ref.current = setTimeout(() => {
30
- onError(PaymentErrorMessageId.ERROR_ABORTING);
30
+ onError(PaymentErrorMessageId.ERROR_ABORT);
31
31
  }, 2000);
32
32
  break;
33
33
  }
@@ -9,10 +9,7 @@ const PaymentInterface = ({ onError, onSuccess }: PaymentInterfaceProps) => (
9
9
  >
10
10
  Simulate payment failure
11
11
  </button>
12
- <button
13
- data-testid="payment-abort"
14
- onClick={() => onError(PaymentErrorMessageId.ERROR_ABORTING)}
15
- >
12
+ <button data-testid="payment-abort" onClick={() => onError(PaymentErrorMessageId.ERROR_ABORT)}>
16
13
  Simulate payment abort
17
14
  </button>
18
15
  <button data-testid="payment-success" onClick={onSuccess}>
@@ -1,10 +1,13 @@
1
1
  export enum PaymentErrorMessageId {
2
2
  ERROR_ABORT = 'errorAbort',
3
- ERROR_ABORTING = 'errorAborting',
3
+ ERROR_DEFAULT = 'errorDefault',
4
+ }
5
+
6
+ export enum SubscriptionErrorMessageId {
7
+ ERROR_ABORT = 'errorAbort',
4
8
  ERROR_ADDRESS = 'errorAddress',
5
9
  ERROR_DEFAULT = 'errorDefault',
6
10
  ERROR_FULL_PRODUCT = 'errorFullProduct',
7
- ERROR_TERMS = 'errorTerms',
8
11
  }
9
12
 
10
13
  export enum PaymentProviders {
@@ -1,3 +1,16 @@
1
+ .payment-schedule__grid {
2
+ .payment-schedule__cell {
3
+ &--wrapped {
4
+ white-space: normal;
5
+ }
6
+
7
+ &--alignRight {
8
+ display: flex;
9
+ justify-content: flex-end;
10
+ }
11
+ }
12
+ }
13
+
1
14
  .status-pill {
2
15
  display: inline-flex;
3
16
  align-items: center;
@@ -1,91 +1,71 @@
1
- import { DataList } from '@openfun/cunningham-react';
1
+ import { DataGrid } from '@openfun/cunningham-react';
2
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
2
3
  import { StringHelper } from 'utils/StringHelper';
4
+ import { PaymentSchedule, PaymentScheduleState } from 'types/Joanie';
5
+ import useDateFormat from 'hooks/useDateFormat';
6
+
7
+ type Props = {
8
+ schedule: PaymentSchedule;
9
+ };
10
+
11
+ const messages = defineMessages({
12
+ withdrawnAt: {
13
+ id: 'components.PaymentScheduleGrid.withdrawnAt',
14
+ defaultMessage: 'Withdrawn on {date}',
15
+ description: 'Label displayed to explain when the installment will be withdrawn.',
16
+ },
17
+ });
18
+
19
+ export const PaymentScheduleGrid = ({ schedule }: Props) => {
20
+ const intl = useIntl();
21
+ const formatDate = useDateFormat();
3
22
 
4
- export const PaymentScheduleGrid = () => {
5
23
  return (
6
24
  <div className="payment-schedule__grid">
7
- <DataList
25
+ <DataGrid
26
+ displayHeader={false}
8
27
  columns={[
28
+ { field: 'index', size: 10 },
29
+ { field: 'amount', size: 90 },
9
30
  {
10
- id: 'date',
11
- renderCell: (context) =>
12
- context.row.id === 'total' ? <strong>{context.row.date}</strong> : context.row.date,
31
+ field: 'date',
32
+ renderCell: ({ row }) => (
33
+ <span className="payment-schedule__cell--wrapped">
34
+ <FormattedMessage {...messages.withdrawnAt} values={{ date: row.date }} />
35
+ </span>
36
+ ),
13
37
  },
14
38
  {
15
- id: 'amount',
16
- renderCell: (context) =>
17
- context.row.id === 'total' ? (
18
- <strong>{context.row.amount}</strong>
39
+ id: 'state',
40
+ renderCell: ({ row }) =>
41
+ row.state ? (
42
+ <div className="payment-schedule__cell--alignRight">
43
+ <StatusPill state={row.state} />
44
+ </div>
19
45
  ) : (
20
- context.row.amount
46
+ ''
21
47
  ),
22
48
  },
23
- {
24
- id: 'status',
25
- renderCell: (context) =>
26
- context.row.status ? <StatusPill status={context.row.status} /> : '',
27
- },
28
- { field: 'message' },
29
- ]}
30
- rows={[
31
- {
32
- id: '1',
33
- date: '2023-03-15',
34
- amount: '€ 100.00',
35
- status: PaymentScheduleStatus.PAID,
36
- message: 'First payment (30%)',
37
- },
38
- {
39
- id: '2',
40
- date: '2023-04-15',
41
- amount: '€ 100.00',
42
- status: PaymentScheduleStatus.REQUIRE_PAYMENT,
43
- message: 'Periodic',
44
- },
45
- {
46
- id: '3',
47
- date: '2023-05-15',
48
- amount: '€ 100.00',
49
- status: PaymentScheduleStatus.FAILED,
50
- message: 'Periodic',
51
- },
52
- {
53
- id: '4',
54
- date: '2023-06-15',
55
- amount: '€ 100.00',
56
- status: PaymentScheduleStatus.INCOMING,
57
- message: 'Periodic',
58
- },
59
- {
60
- id: '5',
61
- date: '2023-06-15',
62
- amount: '€ 100.00',
63
- status: PaymentScheduleStatus.PENDING,
64
- message: 'Periodic',
65
- },
66
- {
67
- id: 'total',
68
- date: 'Total',
69
- amount: '€ 1150.00',
70
- },
71
49
  ]}
50
+ rows={schedule.map((installment, index) => ({
51
+ id: installment.id,
52
+ index: index + 1,
53
+ date: formatDate(installment.due_date),
54
+ amount: intl.formatNumber(installment.amount, {
55
+ style: 'currency',
56
+ currency: installment.currency,
57
+ }),
58
+ state: installment.state,
59
+ }))}
72
60
  />
73
61
  </div>
74
62
  );
75
63
  };
76
64
 
77
- export enum PaymentScheduleStatus {
78
- INCOMING = 'incoming',
79
- PENDING = 'pending',
80
- PAID = 'paid',
81
- FAILED = 'failed',
82
- REQUIRE_PAYMENT = 'require_payment',
83
- }
84
-
85
- export const StatusPill = ({ status }: { status: PaymentScheduleStatus }) => {
65
+ export const StatusPill = ({ state }: { state: PaymentScheduleState }) => {
86
66
  return (
87
- <span className={`status-pill status-pill--${status}`}>
88
- {StringHelper.capitalizeFirst(status.replace('_', ' '))}
67
+ <span className={`status-pill status-pill--${state}`}>
68
+ {StringHelper.capitalizeFirst(state.replace('_', ' '))}
89
69
  </span>
90
70
  );
91
71
  };