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
package/js/api/joanie.ts
CHANGED
|
@@ -15,7 +15,8 @@ import context from 'utils/context';
|
|
|
15
15
|
import { JOANIE_API_VERSION } from 'settings';
|
|
16
16
|
import { ResourcesQuery } from 'hooks/useResources';
|
|
17
17
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
18
|
-
import { Maybe } from 'types/utils';
|
|
18
|
+
import { Maybe, Nullable } from 'types/utils';
|
|
19
|
+
import { PaymentSchedule } from 'types/Joanie';
|
|
19
20
|
import { checkStatus, getFileFromResponse } from './utils';
|
|
20
21
|
|
|
21
22
|
/*
|
|
@@ -78,9 +79,9 @@ export const getRoutes = () => {
|
|
|
78
79
|
},
|
|
79
80
|
creditCards: {
|
|
80
81
|
get: `${baseUrl}/credit-cards/:id/`,
|
|
81
|
-
create: `${baseUrl}/credit-cards/`,
|
|
82
82
|
update: `${baseUrl}/credit-cards/:id/`,
|
|
83
83
|
delete: `${baseUrl}/credit-cards/:id/`,
|
|
84
|
+
tokenize: `${baseUrl}/credit-cards/tokenize-card/`,
|
|
84
85
|
},
|
|
85
86
|
addresses: {
|
|
86
87
|
get: `${baseUrl}/addresses/:id/`,
|
|
@@ -89,14 +90,15 @@ export const getRoutes = () => {
|
|
|
89
90
|
delete: `${baseUrl}/addresses/:id/`,
|
|
90
91
|
},
|
|
91
92
|
orders: {
|
|
92
|
-
|
|
93
|
+
cancel: `${baseUrl}/orders/:id/cancel/`,
|
|
93
94
|
create: `${baseUrl}/orders/`,
|
|
94
|
-
submit: `${baseUrl}/orders/:id/submit/`,
|
|
95
95
|
get: `${baseUrl}/orders/:id/`,
|
|
96
96
|
invoice: {
|
|
97
97
|
download: `${baseUrl}/orders/:id/invoice/`,
|
|
98
98
|
},
|
|
99
99
|
submit_for_signature: `${baseUrl}/orders/:id/submit_for_signature/`,
|
|
100
|
+
submit_installment_payment: `${baseUrl}/orders/:id/submit_installment_payment/`,
|
|
101
|
+
set_payment_method: `${baseUrl}/orders/:id/payment-method/`,
|
|
100
102
|
},
|
|
101
103
|
certificates: {
|
|
102
104
|
download: `${baseUrl}/certificates/:id/download/`,
|
|
@@ -141,6 +143,9 @@ export const getRoutes = () => {
|
|
|
141
143
|
},
|
|
142
144
|
products: {
|
|
143
145
|
get: `${baseUrl}/courses/:course_id/products/:id/`,
|
|
146
|
+
paymentSchedule: {
|
|
147
|
+
get: `${baseUrl}/courses/:course_id/products/:id/payment-schedule/`,
|
|
148
|
+
},
|
|
144
149
|
},
|
|
145
150
|
orders: {
|
|
146
151
|
get: `${baseUrl}/courses/:course_id/orders/:id/`,
|
|
@@ -224,11 +229,6 @@ const API = (): Joanie.API => {
|
|
|
224
229
|
get: async (filters?: ResourcesQuery) => {
|
|
225
230
|
return fetchWithJWT(buildApiUrl(ROUTES.user.creditCards.get, filters)).then(checkStatus);
|
|
226
231
|
},
|
|
227
|
-
create: async (creditCard) =>
|
|
228
|
-
fetchWithJWT(ROUTES.user.creditCards.create, {
|
|
229
|
-
method: 'POST',
|
|
230
|
-
body: JSON.stringify(creditCard),
|
|
231
|
-
}).then(checkStatus),
|
|
232
232
|
update: async ({ id, ...creditCard }) => {
|
|
233
233
|
return fetchWithJWT(ROUTES.user.creditCards.update.replace(':id', id), {
|
|
234
234
|
method: 'PUT',
|
|
@@ -239,6 +239,8 @@ const API = (): Joanie.API => {
|
|
|
239
239
|
fetchWithJWT(ROUTES.user.creditCards.delete.replace(':id', id), {
|
|
240
240
|
method: 'DELETE',
|
|
241
241
|
}).then(checkStatus),
|
|
242
|
+
tokenize: async () =>
|
|
243
|
+
fetchWithJWT(ROUTES.user.creditCards.tokenize, { method: 'POST' }).then(checkStatus),
|
|
242
244
|
},
|
|
243
245
|
addresses: {
|
|
244
246
|
get: (id?: string) => {
|
|
@@ -260,10 +262,9 @@ const API = (): Joanie.API => {
|
|
|
260
262
|
}).then(checkStatus),
|
|
261
263
|
},
|
|
262
264
|
orders: {
|
|
263
|
-
|
|
264
|
-
return fetchWithJWT(ROUTES.user.orders.
|
|
265
|
+
cancel: async (id) => {
|
|
266
|
+
return fetchWithJWT(ROUTES.user.orders.cancel.replace(':id', id), {
|
|
265
267
|
method: 'POST',
|
|
266
|
-
body: payment_id ? JSON.stringify({ payment_id }) : undefined,
|
|
267
268
|
}).then(checkStatus);
|
|
268
269
|
},
|
|
269
270
|
create: async (payload) =>
|
|
@@ -271,11 +272,6 @@ const API = (): Joanie.API => {
|
|
|
271
272
|
method: 'POST',
|
|
272
273
|
body: JSON.stringify(payload),
|
|
273
274
|
}).then(checkStatus),
|
|
274
|
-
submit: async ({ id, ...payload }) =>
|
|
275
|
-
fetchWithJWT(ROUTES.user.orders.submit.replace(':id', id), {
|
|
276
|
-
method: 'PATCH',
|
|
277
|
-
body: JSON.stringify(payload),
|
|
278
|
-
}).then(checkStatus),
|
|
279
275
|
get: async (filters) => {
|
|
280
276
|
return fetchWithJWT(buildApiUrl(ROUTES.user.orders.get, filters)).then(checkStatus);
|
|
281
277
|
},
|
|
@@ -291,6 +287,16 @@ const API = (): Joanie.API => {
|
|
|
291
287
|
fetchWithJWT(ROUTES.user.orders.submit_for_signature.replace(':id', id), {
|
|
292
288
|
method: 'POST',
|
|
293
289
|
}).then(checkStatus),
|
|
290
|
+
submit_installment_payment: async (id, payload) =>
|
|
291
|
+
fetchWithJWT(ROUTES.user.orders.submit_installment_payment.replace(':id', id), {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
body: JSON.stringify(payload),
|
|
294
|
+
}).then(checkStatus),
|
|
295
|
+
set_payment_method: async ({ id, ...payload }) =>
|
|
296
|
+
fetchWithJWT(ROUTES.user.orders.set_payment_method.replace(':id', id), {
|
|
297
|
+
method: 'POST',
|
|
298
|
+
body: JSON.stringify(payload),
|
|
299
|
+
}).then(checkStatus),
|
|
294
300
|
},
|
|
295
301
|
enrollments: {
|
|
296
302
|
create: async (payload) =>
|
|
@@ -418,6 +424,25 @@ const API = (): Joanie.API => {
|
|
|
418
424
|
|
|
419
425
|
return fetchWithJWT(buildApiUrl(ROUTES.courses.products.get, filters)).then(checkStatus);
|
|
420
426
|
},
|
|
427
|
+
paymentSchedule: {
|
|
428
|
+
get: async (
|
|
429
|
+
filters?: Joanie.CourseProductQueryFilters,
|
|
430
|
+
): Promise<Nullable<PaymentSchedule>> => {
|
|
431
|
+
if (!filters) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
'A course code and a product id are required to fetch a course product',
|
|
434
|
+
);
|
|
435
|
+
} else if (!filters.course_id) {
|
|
436
|
+
throw new Error('A course code is required to fetch a course product');
|
|
437
|
+
} else if (!filters.id) {
|
|
438
|
+
throw new Error('A product id is required to fetch a course product');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return fetchWithJWT(
|
|
442
|
+
buildApiUrl(ROUTES.courses.products.paymentSchedule.get, filters),
|
|
443
|
+
).then(checkStatus);
|
|
444
|
+
},
|
|
445
|
+
},
|
|
421
446
|
},
|
|
422
447
|
orders: {
|
|
423
448
|
get: async (filters?: Joanie.CourseOrderResourceQuery) => {
|
package/js/api/lms/dummy.ts
CHANGED
|
@@ -68,7 +68,7 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
|
|
|
68
68
|
|
|
69
69
|
const dummyOpenEdxApiProfile: OpenEdxApiProfile = {
|
|
70
70
|
username: 'j_do',
|
|
71
|
-
name: 'John
|
|
71
|
+
name: 'John Doe',
|
|
72
72
|
email: 'j.do@whois.net',
|
|
73
73
|
country: 'fr',
|
|
74
74
|
level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
|
|
@@ -112,17 +112,6 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
|
|
|
112
112
|
account: {
|
|
113
113
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
114
114
|
get: (username: string): Promise<OpenEdxApiProfile> => {
|
|
115
|
-
return Promise.resolve({
|
|
116
|
-
username: 'j_do',
|
|
117
|
-
name: 'John Do',
|
|
118
|
-
email: 'j.do@whois.net',
|
|
119
|
-
country: 'fr',
|
|
120
|
-
level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
|
|
121
|
-
gender: OpenEdxGender.MALE,
|
|
122
|
-
year_of_birth: '1971',
|
|
123
|
-
'pref-lang': OpenEdxLanguageIsoCode.ENGLISH,
|
|
124
|
-
language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
|
|
125
|
-
} as OpenEdxApiProfile);
|
|
126
115
|
return Promise.resolve(dummyOpenEdxApiProfile);
|
|
127
116
|
},
|
|
128
117
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -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
|
};
|
|
@@ -220,11 +221,13 @@ const ContractFrameContent = ({
|
|
|
220
221
|
|
|
221
222
|
const renderLoadingContract = () => {
|
|
222
223
|
return (
|
|
223
|
-
<div className="
|
|
224
|
+
<div className="ContractFrame__container">
|
|
224
225
|
<h3 className="ContractFrame__caption">
|
|
225
226
|
<FormattedMessage {...messages.loadingContract} />
|
|
226
227
|
</h3>
|
|
227
|
-
<
|
|
228
|
+
<div className="ContractFrame__footer">
|
|
229
|
+
<Loader />
|
|
230
|
+
</div>
|
|
228
231
|
</div>
|
|
229
232
|
);
|
|
230
233
|
};
|
|
@@ -256,20 +259,20 @@ const ContractFrameContent = ({
|
|
|
256
259
|
</Suspense>
|
|
257
260
|
)}
|
|
258
261
|
{step === ContractSteps.POLLING && (
|
|
259
|
-
<div className="
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
<div className="ContractFrame__container">
|
|
263
|
+
<h3 className="ContractFrame__caption">
|
|
264
|
+
<FormattedMessage {...messages.polling} />
|
|
265
|
+
</h3>
|
|
266
|
+
<p className="ContractFrame__content">
|
|
267
|
+
<FormattedMessage {...messages.pollingDescription} />
|
|
268
|
+
</p>
|
|
269
|
+
<div className="ContractFrame__footer">
|
|
270
|
+
<Loader />
|
|
267
271
|
</div>
|
|
268
|
-
<Loader />
|
|
269
272
|
</div>
|
|
270
273
|
)}
|
|
271
274
|
{step === ContractSteps.FINISHED && (
|
|
272
|
-
<div className="
|
|
275
|
+
<div className="ContractFrame__container">
|
|
273
276
|
<SuccessIcon />
|
|
274
277
|
<h3 className="ContractFrame__caption">
|
|
275
278
|
<FormattedMessage {...messages.finishedCaption} />
|
|
@@ -277,9 +280,11 @@ const ContractFrameContent = ({
|
|
|
277
280
|
<p className="ContractFrame__content">
|
|
278
281
|
<FormattedMessage {...messages.finishedDescription} />
|
|
279
282
|
</p>
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
+
<div className="ContractFrame__footer">
|
|
284
|
+
<Button onClick={onClose}>
|
|
285
|
+
<FormattedMessage {...messages.finishedButton} />
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
283
288
|
</div>
|
|
284
289
|
)}
|
|
285
290
|
</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
|
}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { screen, within } from '@testing-library/react';
|
|
2
|
-
import {
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { faker } from '@faker-js/faker';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
|
-
import { CreditCardSelector } from 'components/SaleTunnel/CreditCardSelector/index';
|
|
8
7
|
import { render } from 'utils/test/render';
|
|
9
8
|
import { CreditCard } from 'types/Joanie';
|
|
10
|
-
import {
|
|
11
|
-
CredentialOrderFactory,
|
|
12
|
-
CreditCardFactory,
|
|
13
|
-
ProductFactory,
|
|
14
|
-
} from 'utils/test/factories/joanie';
|
|
15
|
-
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
9
|
+
import { CreditCardFactory } from 'utils/test/factories/joanie';
|
|
16
10
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
17
11
|
import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
|
|
18
|
-
import {
|
|
12
|
+
import { SaleTunnelContextType } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
13
|
+
import { CreditCardSelector } from 'components/CreditCardSelector/index';
|
|
19
14
|
|
|
20
15
|
jest.mock('utils/context', () => ({
|
|
21
16
|
__esModule: true,
|
|
@@ -43,30 +38,10 @@ describe('CreditCardSelector', () => {
|
|
|
43
38
|
|
|
44
39
|
const Wrapper = () => {
|
|
45
40
|
const [creditCard, setCreditCard] = useState<CreditCard>();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
product: ProductFactory().one(),
|
|
51
|
-
props: {} as SaleTunnelProps,
|
|
52
|
-
setBillingAddress: jest.fn(),
|
|
53
|
-
creditCard,
|
|
54
|
-
setCreditCard,
|
|
55
|
-
onPaymentSuccess: jest.fn(),
|
|
56
|
-
step: SaleTunnelStep.PAYMENT,
|
|
57
|
-
registerSubmitCallback: jest.fn(),
|
|
58
|
-
unregisterSubmitCallback: jest.fn(),
|
|
59
|
-
runSubmitCallbacks: jest.fn(),
|
|
60
|
-
}),
|
|
61
|
-
[creditCard],
|
|
62
|
-
);
|
|
63
|
-
contextRef.current = context;
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<SaleTunnelContext.Provider value={context}>
|
|
67
|
-
<CreditCardSelector />
|
|
68
|
-
</SaleTunnelContext.Provider>
|
|
69
|
-
);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
contextRef.current.creditCard = creditCard;
|
|
43
|
+
}, [creditCard]);
|
|
44
|
+
return <CreditCardSelector creditCard={creditCard} setCreditCard={setCreditCard} />;
|
|
70
45
|
};
|
|
71
46
|
|
|
72
47
|
return { contextRef, Wrapper };
|
|
@@ -77,17 +52,12 @@ describe('CreditCardSelector', () => {
|
|
|
77
52
|
const { Wrapper } = buildWrapper();
|
|
78
53
|
render(<Wrapper />);
|
|
79
54
|
|
|
80
|
-
screen.getByRole('heading', {
|
|
81
|
-
name: 'Payment method',
|
|
82
|
-
});
|
|
83
|
-
screen.getByText('Choose your payment method or add a new one during the payment.');
|
|
84
|
-
|
|
85
55
|
// During loading state, the spinner should be displayed and the current selected card should not be displayed.
|
|
86
|
-
expect(screen.queryByText('
|
|
56
|
+
expect(screen.queryByText('Use another credit card')).not.toBeInTheDocument();
|
|
87
57
|
await expectSpinner();
|
|
88
58
|
await expectNoSpinner();
|
|
89
59
|
|
|
90
|
-
screen.getByText('Use another credit card
|
|
60
|
+
screen.getByText('Use another credit card');
|
|
91
61
|
|
|
92
62
|
// As the user has no credit card, the edit button should not be displayed.
|
|
93
63
|
expect(
|
|
@@ -236,7 +206,7 @@ describe('CreditCardSelector', () => {
|
|
|
236
206
|
await user.click(editButton);
|
|
237
207
|
|
|
238
208
|
const radio = screen.getByRole('radio', {
|
|
239
|
-
name: /Use another credit card
|
|
209
|
+
name: /Use another credit card/i,
|
|
240
210
|
});
|
|
241
211
|
await user.click(radio);
|
|
242
212
|
|
|
@@ -246,7 +216,7 @@ describe('CreditCardSelector', () => {
|
|
|
246
216
|
await user.click(submitButton);
|
|
247
217
|
|
|
248
218
|
expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
|
|
249
|
-
screen.getByText('Use another credit card
|
|
219
|
+
screen.getByText('Use another credit card');
|
|
250
220
|
expect(contextRef.current.creditCard).toBeUndefined();
|
|
251
221
|
});
|
|
252
222
|
|
|
@@ -274,15 +244,15 @@ describe('CreditCardSelector', () => {
|
|
|
274
244
|
await screen.findByTestId('credit-card-' + mainCreditCard.id);
|
|
275
245
|
screen.getByText(mainCreditCard.title!);
|
|
276
246
|
screen.getByText('Ends with •••• ' + mainCreditCard.last_numbers);
|
|
277
|
-
expect(screen.queryByText('Add new credit card during payment')).not.toBeInTheDocument();
|
|
278
247
|
expect(contextRef.current.creditCard!.id).toEqual(mainCreditCard.id);
|
|
279
248
|
|
|
280
249
|
const user = userEvent.setup();
|
|
281
|
-
const button = screen.getByRole('button', { name: /use another credit card
|
|
250
|
+
const button = screen.getByRole('button', { name: /use another credit card/i });
|
|
282
251
|
await user.click(button);
|
|
283
252
|
|
|
284
253
|
expect(screen.queryByTestId('credit-card-selector-modal')).not.toBeInTheDocument();
|
|
285
|
-
screen.getByText('Use another credit card
|
|
254
|
+
screen.getByText('Use another credit card');
|
|
255
|
+
expect(button).not.toBeInTheDocument();
|
|
286
256
|
expect(contextRef.current.creditCard).toBeUndefined();
|
|
287
257
|
});
|
|
288
258
|
|
|
@@ -13,7 +13,6 @@ import { CreditCardBrandLogo } from 'pages/DashboardCreditCardsManagement/Credit
|
|
|
13
13
|
import { CreditCard } from 'types/Joanie';
|
|
14
14
|
import { useCreditCardsManagement } from 'hooks/useCreditCardsManagement';
|
|
15
15
|
import { Spinner } from 'components/Spinner';
|
|
16
|
-
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
17
16
|
import { CreditCardExpirationStatus, CreditCardHelper } from 'utils/CreditCardHelper';
|
|
18
17
|
import { useMatchMediaLg } from 'hooks/useMatchMedia';
|
|
19
18
|
|
|
@@ -33,20 +32,10 @@ const messages = defineMessages({
|
|
|
33
32
|
description: 'Text to show the credit card expired date',
|
|
34
33
|
defaultMessage: 'Expired since {month}/{year}',
|
|
35
34
|
},
|
|
36
|
-
title: {
|
|
37
|
-
id: 'components.SaleTunnel.CreditCardSelector.title',
|
|
38
|
-
description: 'Title for the credit card section',
|
|
39
|
-
defaultMessage: 'Payment method',
|
|
40
|
-
},
|
|
41
|
-
description: {
|
|
42
|
-
id: 'components.SaleTunnel.CreditCardSelector.description',
|
|
43
|
-
description: 'Description for the credit card section',
|
|
44
|
-
defaultMessage: 'Choose your payment method or add a new one during the payment.',
|
|
45
|
-
},
|
|
46
35
|
creditCardEmptyInlineDescription: {
|
|
47
36
|
id: 'components.SaleTunnel.CreditCardSelector.creditCardEmptyInlineDescription',
|
|
48
37
|
description: 'Description for the empty credit card inline',
|
|
49
|
-
defaultMessage: 'Use another credit card
|
|
38
|
+
defaultMessage: 'Use another credit card',
|
|
50
39
|
},
|
|
51
40
|
modalTitle: {
|
|
52
41
|
id: 'components.SaleTunnel.CreditCardSelector.modalTitle',
|
|
@@ -71,7 +60,19 @@ const messages = defineMessages({
|
|
|
71
60
|
},
|
|
72
61
|
});
|
|
73
62
|
|
|
74
|
-
export
|
|
63
|
+
export interface CreditCardSelectorProps {
|
|
64
|
+
creditCard?: CreditCard;
|
|
65
|
+
setCreditCard: (creditCard?: CreditCard) => void;
|
|
66
|
+
quickRemove?: boolean;
|
|
67
|
+
allowEdit?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const CreditCardSelector = ({
|
|
71
|
+
creditCard,
|
|
72
|
+
setCreditCard,
|
|
73
|
+
allowEdit = true,
|
|
74
|
+
quickRemove = true,
|
|
75
|
+
}: CreditCardSelectorProps) => {
|
|
75
76
|
const intl = useIntl();
|
|
76
77
|
const modal = useModal();
|
|
77
78
|
const isMobile = useMatchMediaLg();
|
|
@@ -81,8 +82,6 @@ export const CreditCardSelector = () => {
|
|
|
81
82
|
items: creditCards,
|
|
82
83
|
} = useCreditCardsManagement();
|
|
83
84
|
|
|
84
|
-
const { creditCard, setCreditCard } = useSaleTunnelContext();
|
|
85
|
-
|
|
86
85
|
const getDefaultCreditCard = () => {
|
|
87
86
|
if (creditCards.length === 0) {
|
|
88
87
|
return;
|
|
@@ -102,12 +101,6 @@ export const CreditCardSelector = () => {
|
|
|
102
101
|
|
|
103
102
|
return (
|
|
104
103
|
<div className="credit-card-selector">
|
|
105
|
-
<h4 className="block-title mb-t">
|
|
106
|
-
<FormattedMessage {...messages.title} />
|
|
107
|
-
</h4>
|
|
108
|
-
<div className="description mb-s">
|
|
109
|
-
<FormattedMessage {...messages.description} />
|
|
110
|
-
</div>
|
|
111
104
|
{fetching ? (
|
|
112
105
|
<Spinner />
|
|
113
106
|
) : (
|
|
@@ -115,7 +108,7 @@ export const CreditCardSelector = () => {
|
|
|
115
108
|
<div className="credit-card-selector__content">
|
|
116
109
|
{creditCard ? <CreditCardInline creditCard={creditCard} /> : <CreditCardEmptyInline />}
|
|
117
110
|
|
|
118
|
-
{creditCards?.length > 0 && (
|
|
111
|
+
{allowEdit && creditCards?.length > 0 && (
|
|
119
112
|
<Button
|
|
120
113
|
icon={<span className="material-icons">edit</span>}
|
|
121
114
|
color="tertiary-text"
|
|
@@ -125,11 +118,11 @@ export const CreditCardSelector = () => {
|
|
|
125
118
|
/>
|
|
126
119
|
)}
|
|
127
120
|
</div>
|
|
128
|
-
{creditCard && (
|
|
121
|
+
{creditCard && quickRemove && (
|
|
129
122
|
<Button
|
|
130
123
|
onClick={() => setCreditCard(undefined)}
|
|
131
124
|
size="small"
|
|
132
|
-
color="
|
|
125
|
+
color="secondary"
|
|
133
126
|
className="mt-t"
|
|
134
127
|
fullWidth={isMobile}
|
|
135
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';
|