richie-education 2.28.2-dev39 → 2.28.2-dev58
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/.eslintrc.json +11 -2
- package/i18n/locales/ar-SA.json +209 -125
- package/i18n/locales/es-ES.json +210 -126
- package/i18n/locales/fa-IR.json +209 -125
- package/i18n/locales/fr-CA.json +209 -125
- package/i18n/locales/fr-FR.json +209 -125
- package/i18n/locales/ko-KR.json +209 -125
- package/i18n/locales/pt-PT.json +212 -128
- package/i18n/locales/ru-RU.json +209 -125
- package/i18n/locales/vi-VN.json +209 -125
- package/js/api/joanie.ts +14 -17
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +32 -25
- package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
- package/js/components/ContractFrame/_styles.scss +6 -14
- package/js/components/CreditCardSelector/index.spec.tsx +7 -7
- package/js/components/CreditCardSelector/index.tsx +2 -2
- 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/PurchaseButton/index.spec.tsx +69 -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 +75 -41
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
- 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.tsx +5 -0
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
- package/js/components/SaleTunnel/_styles.scss +10 -1
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
- package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
- package/js/components/SaleTunnel/index.spec.tsx +330 -779
- 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/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +1 -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/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Joanie.ts +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- package/js/utils/search/getSuggestionsSection/index.spec.ts +3 -2
- package/js/utils/test/factories/joanie.ts +36 -51
- 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 +7 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
- 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/index.tsx +28 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +4 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
- 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 +27 -27
- package/scss/components/_index.scss +2 -1
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
|
@@ -4,6 +4,7 @@ import fetchMock from 'fetch-mock';
|
|
|
4
4
|
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import { PropsWithChildren } from 'react';
|
|
7
|
+
import { CunninghamProvider } from '@openfun/cunningham-react';
|
|
7
8
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
8
9
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
10
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
@@ -33,11 +34,13 @@ jest.mock('settings', () => ({
|
|
|
33
34
|
describe('<AbstractContractFrame />', () => {
|
|
34
35
|
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
35
36
|
return (
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
<CunninghamProvider>
|
|
38
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
39
|
+
<IntlProvider locale="en">
|
|
40
|
+
<JoanieSessionProvider>{children}</JoanieSessionProvider>
|
|
41
|
+
</IntlProvider>
|
|
42
|
+
</QueryClientProvider>
|
|
43
|
+
</CunninghamProvider>
|
|
41
44
|
);
|
|
42
45
|
};
|
|
43
46
|
|
|
@@ -78,7 +81,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
78
81
|
expect(await screen.findByTestId('dashboard-contract-frame')).toBeInTheDocument();
|
|
79
82
|
|
|
80
83
|
const user = userEvent.setup();
|
|
81
|
-
await user.click(screen.getByRole('button', { name: '
|
|
84
|
+
await user.click(screen.getByRole('button', { name: 'close' }));
|
|
82
85
|
expect(mockOnClose).toHaveBeenCalled();
|
|
83
86
|
});
|
|
84
87
|
|
|
@@ -115,6 +118,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
115
118
|
const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
|
|
116
119
|
const mockOnDone = jest.fn();
|
|
117
120
|
const mockOnClose = jest.fn();
|
|
121
|
+
fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
|
|
118
122
|
|
|
119
123
|
await act(async () => {
|
|
120
124
|
render(
|
|
@@ -176,6 +180,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
176
180
|
const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
|
|
177
181
|
const mockOnDone = jest.fn();
|
|
178
182
|
const mockOnClose = jest.fn();
|
|
183
|
+
fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
|
|
179
184
|
|
|
180
185
|
await act(async () => {
|
|
181
186
|
render(
|
|
@@ -222,7 +227,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
222
227
|
// have been called
|
|
223
228
|
await expectBannerError('An error happened while verifying signature. Please come back later.');
|
|
224
229
|
expect(mockOnDone).not.toHaveBeenCalled();
|
|
225
|
-
button = screen.getByRole('button', { name: '
|
|
230
|
+
button = screen.getByRole('button', { name: 'close' });
|
|
226
231
|
await user.click(button);
|
|
227
232
|
|
|
228
233
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
@@ -237,6 +242,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
237
242
|
const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
|
|
238
243
|
const mockOnDone = jest.fn();
|
|
239
244
|
const mockOnClose = jest.fn();
|
|
245
|
+
fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
|
|
240
246
|
|
|
241
247
|
await act(async () => {
|
|
242
248
|
render(
|
|
@@ -285,7 +291,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
285
291
|
'The signature is taking more time than expected ... please come back later.',
|
|
286
292
|
);
|
|
287
293
|
expect(mockOnDone).not.toHaveBeenCalled();
|
|
288
|
-
button = screen.getByRole('button', { name: '
|
|
294
|
+
button = screen.getByRole('button', { name: 'close' });
|
|
289
295
|
await user.click(button);
|
|
290
296
|
|
|
291
297
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
@@ -300,6 +306,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
300
306
|
const mockCheckSignature = jest.fn(async () => checkSignatureDeferred.promise);
|
|
301
307
|
const mockOnDone = jest.fn();
|
|
302
308
|
const mockOnClose = jest.fn();
|
|
309
|
+
fetchMock.post('https://joanie.endpoint/api/v1.0/signature/notifications/', 200);
|
|
303
310
|
|
|
304
311
|
await act(async () => {
|
|
305
312
|
render(
|
|
@@ -323,7 +330,7 @@ describe('<AbstractContractFrame />', () => {
|
|
|
323
330
|
);
|
|
324
331
|
|
|
325
332
|
// Dummy signature interface should have been rendered
|
|
326
|
-
const button = screen.getByRole('button', { name: '
|
|
333
|
+
const button = screen.getByRole('button', { name: 'close' });
|
|
327
334
|
await user.click(button);
|
|
328
335
|
|
|
329
336
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, { lazy, Suspense, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Button, Loader } from '@openfun/cunningham-react';
|
|
2
|
+
import { Button, Loader, Modal, ModalSize } from '@openfun/cunningham-react';
|
|
3
3
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
4
|
-
import { Modal } from 'components/Modal';
|
|
5
4
|
import { Maybe } from 'types/utils';
|
|
6
5
|
import { CONTRACT_SETTINGS } from 'settings';
|
|
7
6
|
import Banner, { BannerType } from 'components/Banner';
|
|
8
7
|
import { SuccessIcon } from 'components/SuccessIcon';
|
|
8
|
+
import { noop } from 'utils';
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
11
|
/!\ This component should not be used directly, only its implementations should be.
|
|
@@ -63,7 +63,7 @@ export const messages = defineMessages({
|
|
|
63
63
|
},
|
|
64
64
|
finishedDescription: {
|
|
65
65
|
defaultMessage:
|
|
66
|
-
'You will receive an email once your contract will be fully signed. You can now
|
|
66
|
+
'You will receive an email once your contract will be fully signed. You can now finalize your subscription.',
|
|
67
67
|
description: 'Message displayed inside the contract signin modal when the contract is signed.',
|
|
68
68
|
id: 'components.DashboardItem.Order.ContractFrame.finishedDescription',
|
|
69
69
|
},
|
|
@@ -109,15 +109,16 @@ export interface SignatureProps {
|
|
|
109
109
|
invitationLink: string;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const AbstractContractFrame = ({ isOpen, ...props }: Props) => {
|
|
112
|
+
const AbstractContractFrame = ({ isOpen, onClose = noop, ...props }: Props) => {
|
|
113
113
|
return (
|
|
114
114
|
<Modal
|
|
115
115
|
isOpen={isOpen}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
closeOnClickOutside={false}
|
|
117
|
+
closeOnEsc={false}
|
|
118
|
+
onClose={onClose}
|
|
119
|
+
size={ModalSize.LARGE}
|
|
119
120
|
>
|
|
120
|
-
<ContractFrameContent {...props} />
|
|
121
|
+
<ContractFrameContent {...props} onClose={onClose} />
|
|
121
122
|
</Modal>
|
|
122
123
|
);
|
|
123
124
|
};
|
|
@@ -156,7 +157,8 @@ const ContractFrameContent = ({
|
|
|
156
157
|
try {
|
|
157
158
|
const link = await getInvitationLink();
|
|
158
159
|
startStepSign(link);
|
|
159
|
-
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
161
|
+
} catch (_error) {
|
|
160
162
|
setErrored(intl.formatMessage(messages.errorSubmitForSignature));
|
|
161
163
|
}
|
|
162
164
|
};
|
|
@@ -200,7 +202,8 @@ const ContractFrameContent = ({
|
|
|
200
202
|
round++;
|
|
201
203
|
timeoutRef.current = setTimeout(poll, CONTRACT_SETTINGS.pollInterval);
|
|
202
204
|
}
|
|
203
|
-
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
206
|
+
} catch (_error) {
|
|
204
207
|
setErrored(intl.formatMessage(messages.errorPolling));
|
|
205
208
|
}
|
|
206
209
|
}
|
|
@@ -220,11 +223,13 @@ const ContractFrameContent = ({
|
|
|
220
223
|
|
|
221
224
|
const renderLoadingContract = () => {
|
|
222
225
|
return (
|
|
223
|
-
<div className="
|
|
226
|
+
<div className="ContractFrame__container">
|
|
224
227
|
<h3 className="ContractFrame__caption">
|
|
225
228
|
<FormattedMessage {...messages.loadingContract} />
|
|
226
229
|
</h3>
|
|
227
|
-
<
|
|
230
|
+
<div className="ContractFrame__footer">
|
|
231
|
+
<Loader />
|
|
232
|
+
</div>
|
|
228
233
|
</div>
|
|
229
234
|
);
|
|
230
235
|
};
|
|
@@ -256,20 +261,20 @@ const ContractFrameContent = ({
|
|
|
256
261
|
</Suspense>
|
|
257
262
|
)}
|
|
258
263
|
{step === ContractSteps.POLLING && (
|
|
259
|
-
<div className="
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
<div className="ContractFrame__container">
|
|
265
|
+
<h3 className="ContractFrame__caption">
|
|
266
|
+
<FormattedMessage {...messages.polling} />
|
|
267
|
+
</h3>
|
|
268
|
+
<p className="ContractFrame__content">
|
|
269
|
+
<FormattedMessage {...messages.pollingDescription} />
|
|
270
|
+
</p>
|
|
271
|
+
<div className="ContractFrame__footer">
|
|
272
|
+
<Loader />
|
|
267
273
|
</div>
|
|
268
|
-
<Loader />
|
|
269
274
|
</div>
|
|
270
275
|
)}
|
|
271
276
|
{step === ContractSteps.FINISHED && (
|
|
272
|
-
<div className="
|
|
277
|
+
<div className="ContractFrame__container">
|
|
273
278
|
<SuccessIcon />
|
|
274
279
|
<h3 className="ContractFrame__caption">
|
|
275
280
|
<FormattedMessage {...messages.finishedCaption} />
|
|
@@ -277,9 +282,11 @@ const ContractFrameContent = ({
|
|
|
277
282
|
<p className="ContractFrame__content">
|
|
278
283
|
<FormattedMessage {...messages.finishedDescription} />
|
|
279
284
|
</p>
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
285
|
+
<div className="ContractFrame__footer">
|
|
286
|
+
<Button onClick={onClose}>
|
|
287
|
+
<FormattedMessage {...messages.finishedButton} />
|
|
288
|
+
</Button>
|
|
289
|
+
</div>
|
|
283
290
|
</div>
|
|
284
291
|
)}
|
|
285
292
|
</div>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import { Order, AbstractNestedOrder } from 'types/Joanie';
|
|
3
3
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
4
4
|
import AbstractContractFrame, {
|
|
5
5
|
AbstractProps,
|
|
6
6
|
} from 'components/ContractFrame/AbstractContractFrame';
|
|
7
7
|
|
|
8
8
|
interface Props extends AbstractProps {
|
|
9
|
-
order:
|
|
9
|
+
order: Order | AbstractNestedOrder;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const LearnerContractFrame = ({ order, onDone, ...props }: Props) => {
|
|
@@ -17,15 +17,13 @@ iframe#lex-persona {
|
|
|
17
17
|
|
|
18
18
|
.ContractFrame {
|
|
19
19
|
&__modal-body {
|
|
20
|
-
padding:
|
|
20
|
+
padding: 1.5rem 3rem;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
&
|
|
23
|
+
&__container {
|
|
24
24
|
display: flex;
|
|
25
25
|
flex-direction: column;
|
|
26
26
|
align-items: center;
|
|
27
|
-
padding: 3rem 0;
|
|
28
|
-
gap: 4rem;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
&__caption {
|
|
@@ -33,16 +31,6 @@ iframe#lex-persona {
|
|
|
33
31
|
font-size: 1.5rem;
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
&__finished {
|
|
37
|
-
display: flex;
|
|
38
|
-
flex-direction: column;
|
|
39
|
-
align-items: center;
|
|
40
|
-
|
|
41
|
-
button {
|
|
42
|
-
margin-top: 2.5rem;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
&__content {
|
|
47
35
|
color: r-theme-val(contract-frame, content-color);
|
|
48
36
|
font-size: 0.875rem;
|
|
@@ -59,4 +47,8 @@ iframe#lex-persona {
|
|
|
59
47
|
align-items: center;
|
|
60
48
|
gap: 2rem;
|
|
61
49
|
}
|
|
50
|
+
|
|
51
|
+
&__footer {
|
|
52
|
+
margin-top: 2.5rem;
|
|
53
|
+
}
|
|
62
54
|
}
|
|
@@ -53,11 +53,11 @@ describe('CreditCardSelector', () => {
|
|
|
53
53
|
render(<Wrapper />);
|
|
54
54
|
|
|
55
55
|
// During loading state, the spinner should be displayed and the current selected card should not be displayed.
|
|
56
|
-
expect(screen.queryByText('
|
|
56
|
+
expect(screen.queryByText('Use another credit card')).not.toBeInTheDocument();
|
|
57
57
|
await expectSpinner();
|
|
58
58
|
await expectNoSpinner();
|
|
59
59
|
|
|
60
|
-
screen.getByText('Use another credit card
|
|
60
|
+
screen.getByText('Use another credit card');
|
|
61
61
|
|
|
62
62
|
// As the user has no credit card, the edit button should not be displayed.
|
|
63
63
|
expect(
|
|
@@ -206,7 +206,7 @@ describe('CreditCardSelector', () => {
|
|
|
206
206
|
await user.click(editButton);
|
|
207
207
|
|
|
208
208
|
const radio = screen.getByRole('radio', {
|
|
209
|
-
name: /Use another credit card
|
|
209
|
+
name: /Use another credit card/i,
|
|
210
210
|
});
|
|
211
211
|
await user.click(radio);
|
|
212
212
|
|
|
@@ -216,7 +216,7 @@ describe('CreditCardSelector', () => {
|
|
|
216
216
|
await user.click(submitButton);
|
|
217
217
|
|
|
218
218
|
expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
|
|
219
|
-
screen.getByText('Use another credit card
|
|
219
|
+
screen.getByText('Use another credit card');
|
|
220
220
|
expect(contextRef.current.creditCard).toBeUndefined();
|
|
221
221
|
});
|
|
222
222
|
|
|
@@ -244,15 +244,15 @@ describe('CreditCardSelector', () => {
|
|
|
244
244
|
await screen.findByTestId('credit-card-' + mainCreditCard.id);
|
|
245
245
|
screen.getByText(mainCreditCard.title!);
|
|
246
246
|
screen.getByText('Ends with •••• ' + mainCreditCard.last_numbers);
|
|
247
|
-
expect(screen.queryByText('Add new credit card during payment')).not.toBeInTheDocument();
|
|
248
247
|
expect(contextRef.current.creditCard!.id).toEqual(mainCreditCard.id);
|
|
249
248
|
|
|
250
249
|
const user = userEvent.setup();
|
|
251
|
-
const button = screen.getByRole('button', { name: /use another credit card
|
|
250
|
+
const button = screen.getByRole('button', { name: /use another credit card/i });
|
|
252
251
|
await user.click(button);
|
|
253
252
|
|
|
254
253
|
expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
|
|
255
|
-
screen.getByText('Use another credit card
|
|
254
|
+
screen.getByText('Use another credit card');
|
|
255
|
+
expect(button).not.toBeInTheDocument();
|
|
256
256
|
expect(contextRef.current.creditCard).toBeUndefined();
|
|
257
257
|
});
|
|
258
258
|
|
|
@@ -35,7 +35,7 @@ const messages = defineMessages({
|
|
|
35
35
|
creditCardEmptyInlineDescription: {
|
|
36
36
|
id: 'components.SaleTunnel.CreditCardSelector.creditCardEmptyInlineDescription',
|
|
37
37
|
description: 'Description for the empty credit card inline',
|
|
38
|
-
defaultMessage: 'Use another credit card
|
|
38
|
+
defaultMessage: 'Use another credit card',
|
|
39
39
|
},
|
|
40
40
|
modalTitle: {
|
|
41
41
|
id: 'components.SaleTunnel.CreditCardSelector.modalTitle',
|
|
@@ -122,7 +122,7 @@ export const CreditCardSelector = ({
|
|
|
122
122
|
<Button
|
|
123
123
|
onClick={() => setCreditCard(undefined)}
|
|
124
124
|
size="small"
|
|
125
|
-
color="
|
|
125
|
+
color="secondary"
|
|
126
126
|
className="mt-t"
|
|
127
127
|
fullWidth={isMobile}
|
|
128
128
|
>
|
|
@@ -5,8 +5,8 @@ import userEvent from '@testing-library/user-event';
|
|
|
5
5
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
6
6
|
import fetchMock from 'fetch-mock';
|
|
7
7
|
import { faker } from '@faker-js/faker';
|
|
8
|
-
import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
|
|
9
8
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
+
import { ContractFactory, CredentialOrderFactory } from 'utils/test/factories/joanie';
|
|
10
10
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
11
11
|
import JoanieApiProvider from 'contexts/JoanieApiContext';
|
|
12
12
|
import { alert } from 'utils/indirection/window';
|
|
@@ -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
|
|