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.
- package/js/api/joanie.ts +42 -17
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
- package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
- package/js/components/ContractFrame/_styles.scss +6 -14
- package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
- package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +17 -24
- package/js/components/DownloadContractButton/index.spec.tsx +1 -1
- package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
- package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
- package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
- package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
- package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
- package/js/components/PaymentInterfaces/types.ts +5 -2
- package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
- package/js/components/PaymentScheduleGrid/index.tsx +50 -70
- package/js/components/PurchaseButton/index.spec.tsx +84 -37
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
- package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +80 -27
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
- package/js/components/SaleTunnel/_styles.scss +16 -5
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
- package/js/components/SaleTunnel/index.full-process.spec.tsx +116 -48
- package/js/components/SaleTunnel/index.spec.tsx +334 -717
- package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
- package/js/components/SignContractButton/index.spec.tsx +16 -20
- package/js/components/SignContractButton/index.tsx +3 -1
- package/js/hooks/useCreditCards/index.spec.tsx +70 -6
- package/js/hooks/useCreditCards/index.ts +49 -11
- package/js/hooks/useOrders/index.spec.tsx +322 -0
- package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
- package/js/hooks/usePaymentSchedule.tsx +23 -0
- package/js/hooks/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +4 -3
- package/js/index.tsx +2 -0
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
- package/js/settings/settings.test.ts +11 -2
- package/js/types/Joanie.ts +77 -31
- package/js/utils/OrderHelper/index.ts +47 -38
- package/js/utils/test/factories/joanie.ts +66 -68
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +114 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +99 -12
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +126 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +209 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
- package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
- package/package.json +2 -1
- package/scss/components/_index.scss +4 -2
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -338
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
- /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
|
|
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
|
|
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.
|
|
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.
|
|
112
|
+
onError(PaymentErrorMessageId.ERROR_ABORT);
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
|
|
@@ -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
|
-
|
|
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,91 +1,71 @@
|
|
|
1
|
-
import {
|
|
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
|
-
<
|
|
25
|
+
<DataGrid
|
|
26
|
+
displayHeader={false}
|
|
8
27
|
columns={[
|
|
28
|
+
{ field: 'index', size: 10 },
|
|
29
|
+
{ field: 'amount', size: 90 },
|
|
9
30
|
{
|
|
10
|
-
|
|
11
|
-
renderCell: (
|
|
12
|
-
|
|
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: '
|
|
16
|
-
renderCell: (
|
|
17
|
-
|
|
18
|
-
<
|
|
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
|
-
|
|
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
|
|
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--${
|
|
88
|
-
{StringHelper.capitalizeFirst(
|
|
67
|
+
<span className={`status-pill status-pill--${state}`}>
|
|
68
|
+
{StringHelper.capitalizeFirst(state.replace('_', ' '))}
|
|
89
69
|
</span>
|
|
90
70
|
);
|
|
91
71
|
};
|